Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9215c7e1f5 | |||
| b694f233db | |||
| c9e0301e3e | |||
| 3bbacb8fa4 | |||
| d69a95bd29 | |||
| 90b209081c | |||
| 6d145c035c | |||
| bd5ad6e5c2 | |||
| 28005b75af | |||
| 322f81e515 | |||
| ea54a03fae | |||
| 15c55a57a3 | |||
| 056cd12558 | |||
| b0fae0cfd4 | |||
| 44b69b737b | |||
| 56213ef84a | |||
| 64dfc0467d | |||
| c75563a89b | |||
| fb00b517c0 | |||
| df44bd8fd3 | |||
| e9c0ec45db | |||
| d363dfd668 | |||
| c06abbc6f2 | |||
| f499810c7a | |||
| 609d8f5c88 | |||
| 5ebf6c8aec | |||
| d94a11e2e1 | |||
| d4d6185bb4 | |||
| 2df50bcaa0 | |||
| 19ecc7845f | |||
| 142d6ae56f | |||
| 7db39bb482 | |||
| e600a0f702 | |||
| b87654d452 | |||
| a3b0b70a98 | |||
| 81f8aa7eef | |||
| 8f8649c2a8 | |||
| ab0938633a | |||
| d8d4d2464f | |||
| 9d32629f9d | |||
| 6a007a26fe | |||
| 6131f9694a | |||
| 22777bdcfe | |||
| d7946522e9 | |||
| 43262d7b53 | |||
| 80fc1588d3 | |||
| 70ba785364 | |||
| 7fb990f845 | |||
| 9f3bf55c76 | |||
| 83d9747627 | |||
| ae2c789781 | |||
| 81d7ab59ac | |||
| e82d9e075b | |||
| 99af5fdf13 | |||
| b88ce840fa | |||
| 516e136966 | |||
| 051bc8dd67 | |||
| e18f02e2af | |||
| 77885db345 | |||
| 50d504715d | |||
| 28d9c43f85 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.9",
|
||||
"version": "4.3.4",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
---
|
||||
name: version-bump
|
||||
description: Manage semantic version updates for claude-mem project. Handles patch, minor, and major version increments following semantic versioning. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md consistently. Creates git tags.
|
||||
---
|
||||
|
||||
# Version Bump Skill
|
||||
|
||||
IMPORTANT: This skill manages semantic versioning across the claude-mem project. YOU MUST update all FOUR version-tracked files consistently and create a git tag.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Files requiring updates:**
|
||||
1. `package.json` (line 3)
|
||||
2. `.claude-plugin/marketplace.json` (line 13)
|
||||
3. `plugin/.claude-plugin/plugin.json` (line 3)
|
||||
4. `CLAUDE.md` (version history section)
|
||||
|
||||
**Semantic versioning:**
|
||||
- PATCH (x.y.Z): Bugfixes only
|
||||
- MINOR (x.Y.0): New features, backward compatible
|
||||
- MAJOR (X.0.0): Breaking changes
|
||||
|
||||
## Workflow
|
||||
|
||||
When invoked, follow this process:
|
||||
|
||||
### 1. Analyze Changes
|
||||
First, understand what changed:
|
||||
```bash
|
||||
git log --oneline -5
|
||||
git diff HEAD~1
|
||||
```
|
||||
|
||||
### 2. Determine Version Type
|
||||
Ask yourself:
|
||||
- Breaking changes? → MAJOR
|
||||
- New features? → MINOR
|
||||
- Bugfixes only? → PATCH
|
||||
|
||||
If unclear, ASK THE USER explicitly.
|
||||
|
||||
### 3. Calculate New Version
|
||||
From current version in `package.json`:
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
Apply semantic versioning rules:
|
||||
- Patch: increment Z (4.2.8 → 4.2.9)
|
||||
- Minor: increment Y, reset Z (4.2.8 → 4.3.0)
|
||||
- Major: increment X, reset Y and Z (4.2.8 → 5.0.0)
|
||||
|
||||
### 4. Preview Changes
|
||||
BEFORE making changes, show the user:
|
||||
```
|
||||
Current version: 4.2.8
|
||||
New version: 4.2.9 (PATCH)
|
||||
Reason: Fixed database query bug
|
||||
|
||||
Files to update:
|
||||
- package.json: "version": "4.2.9"
|
||||
- marketplace.json: "version": "4.2.9"
|
||||
- plugin.json: "version": "4.2.9"
|
||||
- CLAUDE.md: Add v4.2.9 entry
|
||||
- Git tag: v4.2.9
|
||||
|
||||
Proceed? (yes/no)
|
||||
```
|
||||
|
||||
### 5. Update Files
|
||||
|
||||
**Update package.json:**
|
||||
```json
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.9",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Update .claude-plugin/marketplace.json:**
|
||||
```json
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.9",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Update plugin/.claude-plugin/plugin.json:**
|
||||
```json
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.9",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Update CLAUDE.md:**
|
||||
Add entry at top of Version History section following the template below.
|
||||
|
||||
### 6. Verify Consistency
|
||||
```bash
|
||||
# Check all versions match
|
||||
grep -n '"version"' package.json .claude-plugin/marketplace.json plugin/.claude-plugin/plugin.json
|
||||
# Should show same version in all three files
|
||||
```
|
||||
|
||||
### 7. Test
|
||||
```bash
|
||||
# Verify the plugin loads correctly
|
||||
npm run build
|
||||
# Or whatever build command is appropriate
|
||||
```
|
||||
|
||||
### 8. Commit and Tag
|
||||
```bash
|
||||
# Stage all version files
|
||||
git add package.json .claude-plugin/marketplace.json plugin/.claude-plugin/plugin.json CLAUDE.md plugin/scripts/
|
||||
|
||||
# Commit with descriptive message
|
||||
git commit -m "Release vX.Y.Z: [Brief description]"
|
||||
|
||||
# Create annotated git tag
|
||||
git tag vX.Y.Z -m "Release vX.Y.Z: [Brief description]"
|
||||
|
||||
# Push commit and tags
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
### 9. Create GitHub Release
|
||||
```bash
|
||||
# Create GitHub release from the tag
|
||||
# Extract release notes from CLAUDE.md for the current version
|
||||
gh release create vX.Y.Z --title "vX.Y.Z" --notes "[Paste relevant section from CLAUDE.md]"
|
||||
|
||||
# Or generate notes automatically from commits
|
||||
gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
|
||||
```
|
||||
|
||||
**IMPORTANT**: Always create the GitHub release immediately after pushing the tag. This makes the release discoverable to users and triggers any automated workflows.
|
||||
|
||||
## CLAUDE.md Templates
|
||||
|
||||
### PATCH Version Template
|
||||
```markdown
|
||||
### v4.2.9
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Fixes**:
|
||||
- [Specific bug fixed with file reference: src/db/query.ts:45]
|
||||
- [Impact: what this fixes for users]
|
||||
|
||||
**Technical Details**:
|
||||
- Modified: [file paths with line numbers]
|
||||
- Root cause: [brief explanation]
|
||||
```
|
||||
|
||||
### MINOR Version Template
|
||||
```markdown
|
||||
### v4.3.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
- [Feature name and user benefit]
|
||||
- [How to use: command or API example]
|
||||
|
||||
**Improvements**:
|
||||
- [Enhancement description]
|
||||
|
||||
**Technical Details**:
|
||||
- New files: [paths]
|
||||
- Modified: [paths with line numbers]
|
||||
- Dependencies: [any new dependencies added]
|
||||
```
|
||||
|
||||
### MAJOR Version Template
|
||||
```markdown
|
||||
### v5.0.0
|
||||
**Breaking Changes**:
|
||||
⚠️ [Change 1: what breaks and why]
|
||||
⚠️ [Change 2: what breaks and why]
|
||||
|
||||
**Migration Guide**:
|
||||
1. [Step-by-step instructions]
|
||||
2. [Code examples showing old vs new]
|
||||
3. [Data migration commands if needed]
|
||||
|
||||
**Features**:
|
||||
- [New capabilities enabled by breaking changes]
|
||||
|
||||
**Technical Details**:
|
||||
- Architectural changes: [high-level overview]
|
||||
- Modified: [key files with line numbers]
|
||||
- Removed: [deprecated APIs or features]
|
||||
```
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
**Scenario 1: Bug fix after testing**
|
||||
```
|
||||
User: "Fixed the memory leak in the search function"
|
||||
You: Determine → PATCH
|
||||
Calculate → 4.2.8 → 4.2.9
|
||||
Update all four files
|
||||
Build and commit
|
||||
Create git tag v4.2.9
|
||||
Push commit and tags
|
||||
Create GitHub release v4.2.9
|
||||
CLAUDE.md: Focus on the fix and impact
|
||||
```
|
||||
|
||||
**Scenario 2: New MCP tool added**
|
||||
```
|
||||
User: "Added web search MCP integration"
|
||||
You: Determine → MINOR (new feature)
|
||||
Calculate → 4.2.8 → 4.3.0
|
||||
Update all four files
|
||||
Build and commit
|
||||
Create git tag v4.3.0
|
||||
Push commit and tags
|
||||
Create GitHub release v4.3.0
|
||||
CLAUDE.md: Describe feature and usage
|
||||
```
|
||||
|
||||
**Scenario 3: Database schema redesign**
|
||||
```
|
||||
User: "Rewrote storage layer, old data needs migration"
|
||||
You: Determine → MAJOR (breaking change)
|
||||
Calculate → 4.2.8 → 5.0.0
|
||||
Update all four files
|
||||
Build and commit
|
||||
Create git tag v5.0.0
|
||||
Push commit and tags
|
||||
Create GitHub release v5.0.0
|
||||
CLAUDE.md: Include migration steps
|
||||
```
|
||||
|
||||
## Error Prevention
|
||||
|
||||
**ALWAYS verify:**
|
||||
- [ ] All FOUR files have matching version numbers (package.json, marketplace.json, plugin.json, CLAUDE.md)
|
||||
- [ ] Git tag created with format vX.Y.Z
|
||||
- [ ] GitHub release created from the tag
|
||||
- [ ] CLAUDE.md entry matches version type (patch/minor/major)
|
||||
- [ ] Breaking changes are clearly marked with ⚠️
|
||||
- [ ] File references use format: `path/to/file.ts:line_number`
|
||||
- [ ] CLAUDE.md entry is added at TOP of version history
|
||||
- [ ] Commit and tags pushed to remote
|
||||
|
||||
**NEVER:**
|
||||
- Update only one, two, or three files - ALL FOUR must be updated
|
||||
- Skip the verification step
|
||||
- Forget to create git tag
|
||||
- Forget to create GitHub release
|
||||
- Forget to ask user if version type is unclear
|
||||
- Use vague descriptions in CLAUDE.md
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be explicit about breaking changes** - Users need clear migration paths[(2)](https://docs.claude.com/en/docs/claude-code/plugins-reference#plugin-manifest-schema)
|
||||
2. **Include file references** - Makes debugging easier later[(1)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
3. **Test after bumping** - Ensure version displays correctly[(3)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
4. **Keep CLAUDE.md concise** - Focus on user impact, not implementation minutiae[(1)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
5. **Use consistent formatting** - Follow existing CLAUDE.md style[(1)](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
|
||||
## Reference Commands
|
||||
|
||||
```bash
|
||||
# View current version
|
||||
cat package.json | grep version
|
||||
|
||||
# Check version history
|
||||
head -50 CLAUDE.md | grep "^###"
|
||||
|
||||
# Verify consistency across all version files
|
||||
grep '"version"' package.json .claude-plugin/marketplace.json plugin/.claude-plugin/plugin.json
|
||||
|
||||
# View git tags
|
||||
git tag -l -n1
|
||||
```
|
||||
+3
-1
@@ -7,4 +7,6 @@ node_modules/
|
||||
*.temp
|
||||
.claude/settings.local.json
|
||||
plugin/data/
|
||||
plugin/data.backup/
|
||||
plugin/data.backup/
|
||||
package-lock.json
|
||||
private/
|
||||
@@ -8,6 +8,79 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [4.3.1] - 2025-10-26
|
||||
|
||||
### Fixed
|
||||
- **SessionStart hook context injection**: Fixed context not being injected into new sessions due to npm output pollution
|
||||
- Changed npm loglevel from `--loglevel=error` to `--loglevel=silent` in `plugin/hooks/hooks.json`
|
||||
- npm install stdout/stderr was polluting hook JSON output, preventing proper context injection
|
||||
- Hook now produces clean JSON output for reliable context injection
|
||||
- **Hooks architecture consolidation**: Removed wrapper layer to simplify codebase
|
||||
- Removed `src/bin/hooks/*` wrapper files
|
||||
- Consolidated hook logic directly into `src/hooks/*-hook.ts` files
|
||||
- Fixed double shebang issues (esbuild now adds shebang during build)
|
||||
|
||||
### Technical Details
|
||||
- Modified: `plugin/hooks/hooks.json` (line 25: npm install verbosity)
|
||||
- Removed: All files in `src/bin/hooks/` directory
|
||||
- Root cause: npm stderr/stdout interfering with hook's JSON hookSpecificOutput format
|
||||
|
||||
|
||||
## [4.3.0] - 2025-10-25
|
||||
|
||||
### Added
|
||||
- **Progressive Disclosure Context**: Enhanced context hook with layered memory retrieval system
|
||||
- Layer 1 (Index): Observation titles, token costs, and type indicators at session start
|
||||
- Layer 2 (Details): Full narratives retrieved on-demand via MCP search
|
||||
- Layer 3 (Perfect Recall): Source code and original transcripts
|
||||
- Context hook now displays observations in table format with ID, timestamp, type indicator, title, and token count
|
||||
- Type indicators: 🔴 (critical/gotcha), 🟤 (decision), 🔵 (informational/how-it-works)
|
||||
- Progressive disclosure instructions guide Claude on when to fetch full observation details vs. reading code
|
||||
- Token counts (~200-500 per observation) help Claude make informed retrieval decisions
|
||||
- **Agent Skills documentation**: Added comprehensive documentation on creating and using Claude Code agent skills
|
||||
- **Version bump skill**: Added automated version bump management skill for streamlined releases
|
||||
- **Memory toggle feature planning**: Added design document for future pause/resume recording capability
|
||||
|
||||
### Changed
|
||||
- **Enhanced session summary handling**: Improved timeline rendering and summary organization
|
||||
- **Improved context hook output**: Added structured timeline with session grouping and observation details
|
||||
- **Context token cost**: Increased from ~800 tokens to ~2,500 tokens for richer observation index
|
||||
|
||||
### Fixed
|
||||
- **Cross-platform path detection**: Removed hardcoded macOS-specific paths for project and Claude Code executable (fixes #23)
|
||||
- Removed hardcoded paths in context hook, worker service, and SDK integration
|
||||
- Now uses dynamic path resolution for cross-platform compatibility
|
||||
- Affects: `src/hooks/context.ts`, `src/services/worker-service.ts`, `src/sdk/worker.ts`
|
||||
|
||||
|
||||
## [4.2.11] - 2025-10-25
|
||||
|
||||
### Fixed
|
||||
- **Cross-platform Claude path detection**: Fixed SDK auto-detection failure by implementing explicit `which`/`where` command execution
|
||||
- SDK's automatic Claude path detection was returning undefined
|
||||
- Unix/macOS: Uses `which claude` command to find executable
|
||||
- Windows: Uses `where claude` command (works in both CMD and PowerShell)
|
||||
- Fallback to `CLAUDE_CODE_PATH` environment variable if set
|
||||
- Handles Windows multiple results by taking first match
|
||||
- Worker now logs discovered path for debugging: "Found Claude executable: /path/to/claude"
|
||||
|
||||
### Technical Details
|
||||
- Added `findClaudePath()` helper function using `child_process.execSync`
|
||||
- Platform detection via `process.platform === 'win32'` to choose appropriate command
|
||||
- Updated `src/sdk/worker.ts` and `src/services/worker-service.ts` with explicit path detection
|
||||
- Both files now pass `pathToClaudeCodeExecutable: claudePath` to SDK query
|
||||
|
||||
|
||||
## [4.2.10] - 2025-10-25
|
||||
|
||||
### Fixed
|
||||
- **Windows compatibility**: Removed hardcoded macOS-specific Claude executable path that prevented worker service from running on Windows
|
||||
- Removed hardcoded path: `/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude`
|
||||
- Removed `pathToClaudeCodeExecutable` parameter from SDK query() calls
|
||||
- SDK now automatically detects Claude Code executable path on all platforms
|
||||
- Affects: `src/sdk/worker.ts`, `src/services/worker-service.ts`, `plugin/scripts/worker-service.cjs`
|
||||
|
||||
|
||||
## [4.2.3] - 2025-10-23
|
||||
|
||||
### Security
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Claude-mem is a persistent memory compression system that preserves context across Claude Code sessions. It automatically captures tool usage observations, processes them through the Claude Agent SDK, and makes summaries available to future sessions.
|
||||
|
||||
**Current Version**: 4.2.9
|
||||
**Current Version**: 4.3.4
|
||||
**License**: AGPL-3.0
|
||||
**Author**: Alex Newman (@thedotmack)
|
||||
|
||||
@@ -210,220 +210,102 @@ npm run build && git commit -a -m "Build and update" && git push && cd ~/.claude
|
||||
|
||||
## Version History
|
||||
|
||||
### v4.2.9 (Current)
|
||||
For detailed version history and changelog, see [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
**Current Version**: 4.3.4
|
||||
|
||||
### Recent Highlights
|
||||
|
||||
#### v4.3.4 (2025-11-01)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Documentation**:
|
||||
- Added experimental progressive disclosure context system documentation
|
||||
- New README section explaining the 3-layer memory retrieval approach
|
||||
- Created `EXPERIMENTAL_RELEASE_NOTES.md` with comprehensive testing guide
|
||||
- Created `GITHUB_RELEASE_TEMPLATE.md` for release announcements
|
||||
- Invites users to test `feature/context-with-observations` branch
|
||||
- Progressive disclosure concept: Index (what exists + token costs) → Details (on-demand via MCP) → Perfect recall (source code)
|
||||
- Seeks user feedback on observation-level context injection vs current summary-only approach
|
||||
|
||||
**Purpose**:
|
||||
- Gather real-world feedback before merging experimental context improvements
|
||||
- Test whether showing observation index improves Claude's retrieval decisions
|
||||
- Validate token cost metadata influences Claude's search behavior
|
||||
|
||||
**Files Changed**:
|
||||
- `README.md` - Added experimental feature section
|
||||
- `EXPERIMENTAL_RELEASE_NOTES.md` - Full testing guide and feedback template
|
||||
- `GITHUB_RELEASE_TEMPLATE.md` - Release announcement template
|
||||
- Updated all version references to 4.2.9
|
||||
|
||||
### v4.2.8
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Critical Bugfix**:
|
||||
- Fixed NOT NULL constraint violation that prevented observations and summaries from being stored
|
||||
- Root cause: `SessionStore.getSessionById()` was not selecting `claude_session_id` from database
|
||||
- Worker service received `undefined` for `claude_session_id` when initializing sessions
|
||||
- Result: Database inserts failed with "NOT NULL constraint failed: sdk_sessions.claude_session_id"
|
||||
- Fix: Added `claude_session_id` to SELECT query and return type in `getSessionById()`
|
||||
- Impact: Session ID from hooks now flows correctly: hook → database → worker → SDK agent
|
||||
- Affects: All observation and summary storage operations
|
||||
**Fixes**:
|
||||
- Fixed SessionStart hooks running on session resume (plugin/hooks/hooks.json:4)
|
||||
- Added matcher configuration to only run SessionStart hooks on startup, clear, or compact events
|
||||
- Prevents unnecessary hook execution and improves performance on session resume
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/services/sqlite/SessionStore.ts:711` to include `claude_session_id` in SELECT
|
||||
- Updated return type signature to include `claude_session_id: string` field
|
||||
- Worker service now correctly receives and uses `claude_session_id` from database
|
||||
- System maintains consistency throughout entire session lifecycle
|
||||
- Modified: plugin/hooks/hooks.json:4 (added `"matcher": "startup|clear|compact"`)
|
||||
- Impact: Hooks now skip execution when resuming existing sessions
|
||||
|
||||
**Files Changed**:
|
||||
- `src/services/sqlite/SessionStore.ts` (getSessionById method)
|
||||
|
||||
### v4.2.7
|
||||
#### v4.3.3 (2025-10-27)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Improvements**:
|
||||
- Enhanced data quality with consistent null handling
|
||||
- `extractField()` now returns null for empty/whitespace-only strings
|
||||
- Ensures database stores clean null values instead of empty strings
|
||||
- Improves query efficiency and data consistency
|
||||
- Made session display count configurable via constant (DISPLAY_SESSION_COUNT = 8) in src/hooks/context-hook.ts:11
|
||||
- Added first-time setup detection with helpful user messaging in src/hooks/user-message-hook.ts:12-39
|
||||
- Improved user experience: First install message clarifies why it appears under "Plugin Hook Error"
|
||||
|
||||
**Testing**:
|
||||
- Added comprehensive regression test suite (49 tests)
|
||||
- Tests v4.2.5 summary validation fixes (partial summaries preserved)
|
||||
- Tests v4.2.6 observation validation fixes (partial observations preserved)
|
||||
- Tests edge cases: missing fields, empty fields, whitespace, invalid types
|
||||
- Tests data integrity: concept filtering, type validation, field preservation
|
||||
- New test script: `npm run test:parser`
|
||||
- All 49 tests passing with 100% coverage of critical parser edge cases
|
||||
|
||||
**Code Quality**:
|
||||
- Removed unused `extractFileArray()` function (replaced by `extractArrayElements()`)
|
||||
- Improved function documentation with clearer descriptions
|
||||
- TypeScript diagnostics clean
|
||||
**Fixes**:
|
||||
- Cleaned up profanity in code comments (src/hooks/context-hook.ts:3)
|
||||
- Fixed first-time setup UX by detecting missing node_modules and showing informative message
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/parser.ts:163-169` extractField function
|
||||
- Created `src/sdk/parser.test.ts` with comprehensive regression tests
|
||||
- Added `test:parser` script to package.json
|
||||
- All changes backward compatible with existing database schema
|
||||
- Modified: src/hooks/context-hook.ts:11 (configurable DISPLAY_SESSION_COUNT constant)
|
||||
- Modified: src/hooks/user-message-hook.ts:12-39 (first-time setup detection and messaging)
|
||||
- Modified: plugin/scripts/context-hook.js (rebuilt)
|
||||
- Modified: plugin/scripts/user-message-hook.js (rebuilt)
|
||||
|
||||
### v4.2.6
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Critical Bugfix**:
|
||||
- Fixed overly defensive observation validation that was blocking observations from being saved
|
||||
- Removed validation requiring title, subtitle, and narrative fields
|
||||
- Parser now NEVER skips observations - always saves them
|
||||
- Invalid or missing type defaults to "change" (generic catch-all type)
|
||||
- Prevents critical data loss - partial observations are better than no observations
|
||||
|
||||
**Impact**:
|
||||
- Before: Missing title, subtitle, OR narrative caused entire observation to be discarded
|
||||
- After: ALL observations preserved regardless of field completeness
|
||||
- Even partial observations contain valuable data: concepts, files_read, files_modified, facts
|
||||
- LLMs make mistakes - system must be resilient and save everything
|
||||
- Consistent with v4.2.5 summary fix - partial data is always better than no data
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/parser.ts:52-67` to never skip observations
|
||||
- Uses "change" as fallback type for invalid/missing types (no schema change needed)
|
||||
- Updated ParsedObservation interface to allow null for title, subtitle, narrative
|
||||
- Database schema already supports nullable fields
|
||||
- Parser now matches database schema constraints exactly
|
||||
- Affects `parseObservations()` function used by worker service
|
||||
|
||||
### v4.2.5
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Critical Bugfix**:
|
||||
- Fixed overly defensive summary validation that was blocking summaries from being saved
|
||||
- Removed validation check that returned null when any required fields were missing
|
||||
- Summaries are now always saved when `<summary>` tags are present, even if fields are incomplete
|
||||
- Prevents critical data loss - partial summaries are better than no summaries
|
||||
- Database schema already supports null/empty values for all fields
|
||||
|
||||
**Impact**:
|
||||
- Before: Missing a single field (e.g., `next_steps`) would cause entire summary to be discarded
|
||||
- After: All summaries are preserved, maintaining session context even when incomplete
|
||||
- This fix ensures continuity of the memory compression system
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/parser.ts:137-147` to remove blocking validation
|
||||
- Parser now returns ParsedSummary with whatever fields are available
|
||||
- Affects `parseSummary()` function used by worker service
|
||||
|
||||
### v4.2.4
|
||||
#### v4.3.2 (2025-10-27)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Improvements**:
|
||||
- Enhanced summary prompt clarity and reliability
|
||||
- Removed optional skip_summary functionality (summaries now always generated)
|
||||
- Clarified that summaries are mid-session checkpoints, not session endings
|
||||
- Improved request field instructions to better form descriptive titles
|
||||
- Changed wording from "discovered" to "learned" for consistency
|
||||
- Added user-message-hook for displaying context to users via stderr mechanism (src/hooks/user-message-hook.ts)
|
||||
- Enhanced context visibility: Hook fires simultaneously with context injection, sending duplicate message as "error" so Claude Code displays it to users
|
||||
- Added comprehensive documentation (4 new MDX files covering architecture evolution, context engineering, hooks architecture, and progressive disclosure)
|
||||
- Improved cross-platform path handling in context-hook (src/hooks/context-hook.ts:14)
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/prompts.ts` to remove `WHEN NOT TO SUMMARIZE` section
|
||||
- Added footer text clarifying summaries track progress within ongoing sessions
|
||||
- Changed request field prompt from "Use their original sentiment" to "Form a title that reflects the actual request"
|
||||
- Affects both observation and summary prompt generation
|
||||
- New files:
|
||||
- src/hooks/user-message-hook.ts (stderr-based user-facing context display)
|
||||
- plugin/scripts/user-message-hook.js (built hook executable)
|
||||
- docs/architecture-evolution.mdx (801 lines)
|
||||
- docs/context-engineering.mdx (222 lines)
|
||||
- docs/hooks-architecture.mdx (784 lines)
|
||||
- docs/progressive-disclosure.mdx (655 lines)
|
||||
- Modified:
|
||||
- plugin/hooks/hooks.json:5 (added user-message-hook configuration)
|
||||
- src/hooks/context-hook.ts:14 (improved path handling)
|
||||
- scripts/build-hooks.js:3 (build support for new hook)
|
||||
- Design rationale: Error messages don't get added to context, so we intentionally duplicate context output via stderr for user visibility. This is a temporary workaround until Claude Code potentially adds ability to share messages with both user and context simultaneously.
|
||||
|
||||
### v4.2.3
|
||||
#### v4.3.1 (2025-10-26)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Security**:
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Implemented proper double-quote escaping for FTS5 queries
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts` MCP tools
|
||||
|
||||
**Fixes**:
|
||||
- Fixed ESM/CJS compatibility for getDirname function in src/shared/paths.ts
|
||||
- Detects context using `typeof __dirname !== 'undefined'`
|
||||
- Falls back to `fileURLToPath(import.meta.url)` for ESM modules
|
||||
- Resolves path resolution issues across hook (ESM) and worker (CJS) contexts
|
||||
- Fixed Windows PowerShell compatibility issue with SessionStart hook
|
||||
- Replaced bash-specific test command `[` with cross-platform npm install command
|
||||
- Hook now runs `npm install` with quiet flags (fast and idempotent when dependencies exist)
|
||||
- Fixed SessionStart hook context injection by silencing npm install output (plugin/hooks/hooks.json:25)
|
||||
- Changed npm loglevel from `--loglevel=error` to `--loglevel=silent` to ensure clean JSON output
|
||||
- Consolidated hooks architecture by removing bin/hooks wrapper layer (src/hooks/*-hook.ts)
|
||||
- Fixed double shebang issues in hook executables (esbuild now adds shebang during build)
|
||||
|
||||
**Technical Details**:
|
||||
- SessionSearch.ts now escapes double quotes in FTS5 queries: `query.replace(/"/g, '""')`
|
||||
- Updated `plugin/hooks/hooks.json` SessionStart command to use standard shell syntax
|
||||
- Changed from: `[ ! -d ... ] && cd ... && npm install && node ... || node ...`
|
||||
- Changed to: `cd ... && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ...`
|
||||
- Dependencies are installed in marketplace folder (parent of CLAUDE_PLUGIN_ROOT) where root package.json exists
|
||||
- getDirname function now properly handles both CommonJS (__dirname) and ES modules (import.meta.url)
|
||||
- Modified: plugin/hooks/hooks.json (npm install verbosity)
|
||||
- Removed: src/bin/hooks/* (wrapper layer no longer needed)
|
||||
- Consolidated: Hook logic moved directly into src/hooks/*-hook.ts files
|
||||
- Root cause: npm install stderr/stdout was polluting hook JSON output, preventing context injection
|
||||
|
||||
### v4.2.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
#### v4.3.0 (2025-10-25)
|
||||
- Progressive Disclosure Context: Enhanced context hook with observation timeline and token cost visibility
|
||||
- Session observations now display in table format showing ID, timestamp, type indicators, title, and token counts
|
||||
- Added progressive disclosure usage instructions to guide Claude on when to fetch full observation details vs. reading code
|
||||
- Added Agent Skills documentation and version bump management skill
|
||||
- Cross-platform path improvements: Removed hardcoded paths for project and Claude Code executable (fixes #23)
|
||||
|
||||
**Features**:
|
||||
- User prompt storage with FTS5 full-text search
|
||||
- New `user_prompts` table stores raw user input for every prompt
|
||||
- New `search_user_prompts` MCP tool enables searching actual user requests
|
||||
- Automatic FTS5 indexing of all user prompts for fast retrieval
|
||||
#### v4.2.11 (2025-10-25)
|
||||
- Fixed cross-platform Claude executable path detection using `which`/`where` commands
|
||||
- Full Windows, macOS, and Linux compatibility
|
||||
|
||||
**Benefits**:
|
||||
- Full context reconstruction from user intent → Claude actions → outcomes
|
||||
- Pattern detection for repeated requests (identify when Claude isn't listening)
|
||||
- Improved debugging by tracing from original user words to final implementation
|
||||
- Historical search: "How many times did user ask for X feature?"
|
||||
#### v4.2.8 (2025-10-25)
|
||||
- Fixed NOT NULL constraint violation for claude_session_id
|
||||
|
||||
**Implementation**:
|
||||
- Migration 10: Creates user_prompts table with FTS5 virtual table and sync triggers
|
||||
- UserPromptSubmit hook now saves prompts using claude_session_id (available immediately)
|
||||
- Citations use `claude-mem://user-prompt/{id}` URI scheme
|
||||
#### v4.2.3 (2025-10-23)
|
||||
- Fixed FTS5 injection vulnerability
|
||||
- Fixed Windows PowerShell compatibility
|
||||
|
||||
### v4.1.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
- Graceful session cleanup (marks complete instead of DELETE)
|
||||
- Restored MCP search server from backup
|
||||
- Updated dependencies (claude-agent-sdk 0.1.23, MCP SDK 1.20.1)
|
||||
|
||||
**Fixes**:
|
||||
- `/clear` command now skips cleanup to prevent session interruption
|
||||
- Session workers can finish pending operations naturally
|
||||
|
||||
### v4.0.0
|
||||
**Breaking Changes**:
|
||||
- Data directory relocated to `${CLAUDE_PLUGIN_ROOT}/data/`
|
||||
- Fresh start required (no migration from v3.x)
|
||||
- Worker auto-starts in SessionStart hook
|
||||
|
||||
**Features**:
|
||||
- MCP Search Server with 8 specialized search tools
|
||||
- FTS5 full-text search across observations, sessions, and user prompts
|
||||
- Citation support with `claude-mem://` URIs
|
||||
- HTTP REST API architecture with PM2 management
|
||||
#### v4.0.0 (2025-10-18)
|
||||
- MCP Search Server with FTS5 full-text search
|
||||
- Plugin data directory integration
|
||||
|
||||
**Changes**:
|
||||
- Improved session continuity
|
||||
- Enhanced error handling
|
||||
- Better process cleanup
|
||||
|
||||
### Earlier Versions (v3.x)
|
||||
- v3.9.17: MCP integration, hookSpecificOutput JSON format
|
||||
- v3.7.1: SQLite storage backend
|
||||
- Earlier: Mintlify documentation, statusline support
|
||||
- HTTP REST API architecture with PM2
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/version-4.2.9-green.svg" alt="Version">
|
||||
<img src="https://img.shields.io/badge/version-4.3.1-green.svg" alt="Version">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node">
|
||||
</a>
|
||||
<a href="https://github.com/hesreallyhim/awesome-claude-code">
|
||||
<img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Claude Code">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -40,90 +43,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Experimental Feature: Progressive Disclosure Context
|
||||
|
||||
> **We'd love your feedback!** Test the new context injection system in `feature/context-with-observations`
|
||||
|
||||
### What is Progressive Disclosure?
|
||||
|
||||
Progressive disclosure is a layered approach to memory retrieval that mirrors how humans remember information:
|
||||
|
||||
**Layer 1: Index** (Frontloaded at session start)
|
||||
- **WHAT** exists: Observation titles and session summaries
|
||||
- **COST** of retrieval: Token counts for each observation
|
||||
- **TYPE** indicators: Critical (🔴 gotcha, 🟤 decision) vs informational (🔵 how-it-works)
|
||||
|
||||
**Layer 2: Details** (Retrieved on-demand via MCP search)
|
||||
- Full observation narratives when Claude needs deeper context
|
||||
- Search by concept, file, type, or keyword
|
||||
|
||||
**Layer 3: Perfect Recall** (Code/transcripts)
|
||||
- Source code when implementation details are needed
|
||||
- Original transcripts for exact quotes
|
||||
|
||||
### The Problem It Solves
|
||||
|
||||
**Current version** (v4.2.x): Shows only session-level summaries at startup
|
||||
- ✅ Minimal tokens (~800)
|
||||
- ❌ Claude doesn't know what detailed observations exist
|
||||
- ❌ Often re-reads code to understand past decisions
|
||||
|
||||
**Experimental version**: Shows observation index + session summaries
|
||||
- ✅ Claude sees WHAT learnings exist without loading full content
|
||||
- ✅ Token counts help Claude decide: "fetch details" vs "read code"
|
||||
- ✅ Progressive disclosure instructions teach Claude how to use the system
|
||||
- ⚠️ Higher token cost (~2,500) but potentially more efficient overall
|
||||
|
||||
### How It's Different
|
||||
|
||||
The experimental context hook displays observations in a **table format**:
|
||||
|
||||
```markdown
|
||||
**src/hooks/context.ts**
|
||||
| ID | Time | T | Title | Tokens |
|
||||
|----|------|---|-------|--------|
|
||||
| #2332 | 1:07 AM | 🔴 | Critical Bugfix: Session ID NULL Constraint | ~201 |
|
||||
| #2340 | 1:10 AM | 🟠 | Remove Redundant Summary Section | ~280 |
|
||||
```
|
||||
|
||||
Now Claude knows:
|
||||
- A critical bugfix exists about session IDs (~201 tokens to fetch)
|
||||
- A design decision exists about summary sections (~280 tokens)
|
||||
- Whether to use MCP search or just read the current code
|
||||
|
||||
### Try It Out
|
||||
|
||||
```bash
|
||||
# Clone and checkout the experimental branch
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
git checkout feature/context-with-observations
|
||||
|
||||
# Build the experimental version
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Test the new context hook (from YOUR project directory)
|
||||
cd /path/to/your/project
|
||||
node /path/to/claude-mem/plugin/scripts/context-hook.js
|
||||
|
||||
# Example:
|
||||
# cd ~/my-app
|
||||
# node ~/claude-mem/plugin/scripts/context-hook.js
|
||||
```
|
||||
|
||||
**Important:** Run the context hook from your project's root directory to see context specific to that project.
|
||||
|
||||
**We want to know:**
|
||||
- Does Claude use MCP search more effectively?
|
||||
- Do token counts influence retrieval decisions?
|
||||
- Is the progressive disclosure guidance helpful or noisy?
|
||||
- Does it reduce redundant code reading?
|
||||
|
||||
**📣 Share Your Feedback:** [Open a GitHub Issue](https://github.com/thedotmack/claude-mem/issues/new) with your experience! Tag it with `feedback: progressive-disclosure`
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
Start a new Claude Code session in the terminal and enter the following commands:
|
||||
@@ -138,9 +57,9 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
|
||||
**Key Features:**
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 📊 **FTS5 Search** - Fast full-text search across observations
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
|
||||
---
|
||||
@@ -160,9 +79,15 @@ npx mintlify dev
|
||||
- **[Usage Guide](docs/usage/getting-started.mdx)** - How Claude-Mem works automatically
|
||||
- **[MCP Search Tools](docs/usage/search-tools.mdx)** - Query your project history
|
||||
|
||||
### Best Practices
|
||||
- **[Context Engineering](docs/context-engineering.mdx)** - AI agent context optimization principles
|
||||
- **[Progressive Disclosure](docs/progressive-disclosure.mdx)** - Philosophy behind Claude-Mem's context priming strategy
|
||||
|
||||
### Architecture
|
||||
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow
|
||||
- **[Hooks](docs/architecture/hooks.mdx)** - 5 lifecycle hooks explained
|
||||
- **[Architecture Evolution](docs/architecture-evolution.mdx)** - The journey from v3 to v4
|
||||
- **[Hooks Architecture](docs/hooks-architecture.mdx)** - How Claude-Mem uses lifecycle hooks
|
||||
- **[Hooks Reference](docs/architecture/hooks.mdx)** - 5 lifecycle hooks explained
|
||||
- **[Worker Service](docs/architecture/worker-service.mdx)** - HTTP API & PM2 management
|
||||
- **[Database](docs/architecture/database.mdx)** - SQLite schema & FTS5 search
|
||||
- **[MCP Search](docs/architecture/mcp-search.mdx)** - 7 search tools & examples
|
||||
@@ -232,16 +157,18 @@ See [MCP Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v4.2.3
|
||||
## What's New in v4.3.1
|
||||
|
||||
**Security:**
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
**Critical Fix:**
|
||||
- **SessionStart hook context injection**: Fixed context not being injected into new sessions
|
||||
- npm install output was polluting hook JSON responses
|
||||
- Changed npm loglevel to `--loglevel=silent` for clean output
|
||||
- Context injection now works reliably across all sessions
|
||||
|
||||
**Fixes:**
|
||||
- Fixed ESM/CJS compatibility for getDirname function
|
||||
- Fixed Windows PowerShell compatibility in SessionStart hook
|
||||
- Cross-platform dependency installation now works on Windows, macOS, and Linux
|
||||
**Code Quality:**
|
||||
- Consolidated hooks architecture by removing wrapper layer
|
||||
- Fixed double shebang issues in hook executables
|
||||
- Simplified codebase maintenance
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
@@ -258,6 +185,14 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
## Key Benefits
|
||||
|
||||
### Progressive Disclosure Context
|
||||
- **Layered memory retrieval** mirrors human memory patterns
|
||||
- **Layer 1 (Index)**: See what observations exist with token costs at session start
|
||||
- **Layer 2 (Details)**: Fetch full narratives on-demand via MCP search
|
||||
- **Layer 3 (Perfect Recall)**: Access source code and original transcripts
|
||||
- **Smart decision-making**: Token counts help Claude choose between fetching details or reading code
|
||||
- **Type indicators**: Visual cues (🔴 critical, 🟤 decision, 🔵 informational) highlight observation importance
|
||||
|
||||
### Automatic Memory
|
||||
- Context automatically injected when Claude starts
|
||||
- No manual commands or configuration needed
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.6.0",
|
||||
"commander": "^14.0.0",
|
||||
"glob": "^11.0.3",
|
||||
"gradient-string": "^3.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.15", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.24.1" } }, "sha512-x0UR/YW87lRel3wYVimWjAkUOEGapg/nXx2GYNLbUD1ORsetRHeXZGFdJMuhRkk1Jt9sbn5m/RpT42b5R0pgYg=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||
|
||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||
|
||||
"glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="],
|
||||
|
||||
"gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="],
|
||||
|
||||
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"node-abi": ["node-abi@3.78.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||
|
||||
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||
|
||||
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||
|
||||
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
# Agent Skills
|
||||
|
||||
> Create, manage, and share Skills to extend Claude's capabilities in Claude Code.
|
||||
|
||||
This guide shows you how to create, use, and manage Agent Skills in Claude Code. Skills are modular capabilities that extend Claude's functionality through organized folders containing instructions, scripts, and resources.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Claude Code version 1.0 or later
|
||||
* Basic familiarity with [Claude Code](/en/docs/claude-code/quickstart)
|
||||
|
||||
## What are Agent Skills?
|
||||
|
||||
Agent Skills package expertise into discoverable capabilities. Each Skill consists of a `SKILL.md` file with instructions that Claude reads when relevant, plus optional supporting files like scripts and templates.
|
||||
|
||||
**How Skills are invoked**: Skills are **model-invoked**—Claude autonomously decides when to use them based on your request and the Skill's description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command` to trigger them).
|
||||
|
||||
**Benefits**:
|
||||
|
||||
* Extend Claude's capabilities for your specific workflows
|
||||
* Share expertise across your team via git
|
||||
* Reduce repetitive prompting
|
||||
* Compose multiple Skills for complex tasks
|
||||
|
||||
Learn more in the [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview).
|
||||
|
||||
<Note>
|
||||
For a deep dive into the architecture and real-world applications of Agent Skills, read our engineering blog: [Equipping agents for the real world with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills).
|
||||
</Note>
|
||||
|
||||
## Create a Skill
|
||||
|
||||
Skills are stored as directories containing a `SKILL.md` file.
|
||||
|
||||
### Personal Skills
|
||||
|
||||
Personal Skills are available across all your projects. Store them in `~/.claude/skills/`:
|
||||
|
||||
```bash theme={null}
|
||||
mkdir -p ~/.claude/skills/my-skill-name
|
||||
```
|
||||
|
||||
**Use personal Skills for**:
|
||||
|
||||
* Your individual workflows and preferences
|
||||
* Experimental Skills you're developing
|
||||
* Personal productivity tools
|
||||
|
||||
### Project Skills
|
||||
|
||||
Project Skills are shared with your team. Store them in `.claude/skills/` within your project:
|
||||
|
||||
```bash theme={null}
|
||||
mkdir -p .claude/skills/my-skill-name
|
||||
```
|
||||
|
||||
**Use project Skills for**:
|
||||
|
||||
* Team workflows and conventions
|
||||
* Project-specific expertise
|
||||
* Shared utilities and scripts
|
||||
|
||||
Project Skills are checked into git and automatically available to team members.
|
||||
|
||||
### Plugin Skills
|
||||
|
||||
Skills can also come from [Claude Code plugins](/en/docs/claude-code/plugins). Plugins may bundle Skills that are automatically available when the plugin is installed. These Skills work the same way as personal and project Skills.
|
||||
|
||||
## Write SKILL.md
|
||||
|
||||
Create a `SKILL.md` file with YAML frontmatter and Markdown content:
|
||||
|
||||
```yaml theme={null}
|
||||
---
|
||||
name: your-skill-name
|
||||
description: Brief description of what this Skill does and when to use it
|
||||
---
|
||||
|
||||
# Your Skill Name
|
||||
|
||||
## Instructions
|
||||
Provide clear, step-by-step guidance for Claude.
|
||||
|
||||
## Examples
|
||||
Show concrete examples of using this Skill.
|
||||
```
|
||||
|
||||
**Field requirements**:
|
||||
|
||||
* `name`: Must use lowercase letters, numbers, and hyphens only (max 64 characters)
|
||||
* `description`: Brief description of what the Skill does and when to use it (max 1024 characters)
|
||||
|
||||
The `description` field is critical for Claude to discover when to use your Skill. It should include both what the Skill does and when Claude should use it.
|
||||
|
||||
See the [best practices guide](/en/docs/agents-and-tools/agent-skills/best-practices) for complete authoring guidance including validation rules.
|
||||
|
||||
## Add supporting files
|
||||
|
||||
Create additional files alongside SKILL.md:
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md (required)
|
||||
├── reference.md (optional documentation)
|
||||
├── examples.md (optional examples)
|
||||
├── scripts/
|
||||
│ └── helper.py (optional utility)
|
||||
└── templates/
|
||||
└── template.txt (optional template)
|
||||
```
|
||||
|
||||
Reference these files from SKILL.md:
|
||||
|
||||
````markdown theme={null}
|
||||
For advanced usage, see [reference.md](reference.md).
|
||||
|
||||
Run the helper script:
|
||||
```bash
|
||||
python scripts/helper.py input.txt
|
||||
```
|
||||
````
|
||||
|
||||
Claude reads these files only when needed, using progressive disclosure to manage context efficiently.
|
||||
|
||||
## Restrict tool access with allowed-tools
|
||||
|
||||
Use the `allowed-tools` frontmatter field to limit which tools Claude can use when a Skill is active:
|
||||
|
||||
```yaml theme={null}
|
||||
---
|
||||
name: safe-file-reader
|
||||
description: Read files without making changes. Use when you need read-only file access.
|
||||
allowed-tools: Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Safe File Reader
|
||||
|
||||
This Skill provides read-only file access.
|
||||
|
||||
## Instructions
|
||||
1. Use Read to view file contents
|
||||
2. Use Grep to search within files
|
||||
3. Use Glob to find files by pattern
|
||||
```
|
||||
|
||||
When this Skill is active, Claude can only use the specified tools (Read, Grep, Glob) without needing to ask for permission. This is useful for:
|
||||
|
||||
* Read-only Skills that shouldn't modify files
|
||||
* Skills with limited scope (e.g., only data analysis, no file writing)
|
||||
* Security-sensitive workflows where you want to restrict capabilities
|
||||
|
||||
If `allowed-tools` is not specified, Claude will ask for permission to use tools as normal, following the standard permission model.
|
||||
|
||||
<Note>
|
||||
`allowed-tools` is only supported for Skills in Claude Code.
|
||||
</Note>
|
||||
|
||||
## View available Skills
|
||||
|
||||
Skills are automatically discovered by Claude from three sources:
|
||||
|
||||
* Personal Skills: `~/.claude/skills/`
|
||||
* Project Skills: `.claude/skills/`
|
||||
* Plugin Skills: bundled with installed plugins
|
||||
|
||||
**To view all available Skills**, ask Claude directly:
|
||||
|
||||
```
|
||||
What Skills are available?
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
List all available Skills
|
||||
```
|
||||
|
||||
This will show all Skills from all sources, including plugin Skills.
|
||||
|
||||
**To inspect a specific Skill**, you can also check the filesystem:
|
||||
|
||||
```bash theme={null}
|
||||
# List personal Skills
|
||||
ls ~/.claude/skills/
|
||||
|
||||
# List project Skills (if in a project directory)
|
||||
ls .claude/skills/
|
||||
|
||||
# View a specific Skill's content
|
||||
cat ~/.claude/skills/my-skill/SKILL.md
|
||||
```
|
||||
|
||||
## Test a Skill
|
||||
|
||||
After creating a Skill, test it by asking questions that match your description.
|
||||
|
||||
**Example**: If your description mentions "PDF files":
|
||||
|
||||
```
|
||||
Can you help me extract text from this PDF?
|
||||
```
|
||||
|
||||
Claude autonomously decides to use your Skill if it matches the request—you don't need to explicitly invoke it. The Skill activates automatically based on the context of your question.
|
||||
|
||||
## Debug a Skill
|
||||
|
||||
If Claude doesn't use your Skill, check these common issues:
|
||||
|
||||
### Make description specific
|
||||
|
||||
**Too vague**:
|
||||
|
||||
```yaml theme={null}
|
||||
description: Helps with documents
|
||||
```
|
||||
|
||||
**Specific**:
|
||||
|
||||
```yaml theme={null}
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
```
|
||||
|
||||
Include both what the Skill does and when to use it in the description.
|
||||
|
||||
### Verify file path
|
||||
|
||||
**Personal Skills**: `~/.claude/skills/skill-name/SKILL.md`
|
||||
**Project Skills**: `.claude/skills/skill-name/SKILL.md`
|
||||
|
||||
Check the file exists:
|
||||
|
||||
```bash theme={null}
|
||||
# Personal
|
||||
ls ~/.claude/skills/my-skill/SKILL.md
|
||||
|
||||
# Project
|
||||
ls .claude/skills/my-skill/SKILL.md
|
||||
```
|
||||
|
||||
### Check YAML syntax
|
||||
|
||||
Invalid YAML prevents the Skill from loading. Verify the frontmatter:
|
||||
|
||||
```bash theme={null}
|
||||
cat SKILL.md | head -n 10
|
||||
```
|
||||
|
||||
Ensure:
|
||||
|
||||
* Opening `---` on line 1
|
||||
* Closing `---` before Markdown content
|
||||
* Valid YAML syntax (no tabs, correct indentation)
|
||||
|
||||
### View errors
|
||||
|
||||
Run Claude Code with debug mode to see Skill loading errors:
|
||||
|
||||
```bash theme={null}
|
||||
claude --debug
|
||||
```
|
||||
|
||||
## Share Skills with your team
|
||||
|
||||
**Recommended approach**: Distribute Skills through [plugins](/en/docs/claude-code/plugins).
|
||||
|
||||
To share Skills via plugin:
|
||||
|
||||
1. Create a plugin with Skills in the `skills/` directory
|
||||
2. Add the plugin to a marketplace
|
||||
3. Team members install the plugin
|
||||
|
||||
For complete instructions, see [Add Skills to your plugin](/en/docs/claude-code/plugins#add-skills-to-your-plugin).
|
||||
|
||||
You can also share Skills directly through project repositories:
|
||||
|
||||
### Step 1: Add Skill to your project
|
||||
|
||||
Create a project Skill:
|
||||
|
||||
```bash theme={null}
|
||||
mkdir -p .claude/skills/team-skill
|
||||
# Create SKILL.md
|
||||
```
|
||||
|
||||
### Step 2: Commit to git
|
||||
|
||||
```bash theme={null}
|
||||
git add .claude/skills/
|
||||
git commit -m "Add team Skill for PDF processing"
|
||||
git push
|
||||
```
|
||||
|
||||
### Step 3: Team members get Skills automatically
|
||||
|
||||
When team members pull the latest changes, Skills are immediately available:
|
||||
|
||||
```bash theme={null}
|
||||
git pull
|
||||
claude # Skills are now available
|
||||
```
|
||||
|
||||
## Update a Skill
|
||||
|
||||
Edit SKILL.md directly:
|
||||
|
||||
```bash theme={null}
|
||||
# Personal Skill
|
||||
code ~/.claude/skills/my-skill/SKILL.md
|
||||
|
||||
# Project Skill
|
||||
code .claude/skills/my-skill/SKILL.md
|
||||
```
|
||||
|
||||
Changes take effect the next time you start Claude Code. If Claude Code is already running, restart it to load the updates.
|
||||
|
||||
## Remove a Skill
|
||||
|
||||
Delete the Skill directory:
|
||||
|
||||
```bash theme={null}
|
||||
# Personal
|
||||
rm -rf ~/.claude/skills/my-skill
|
||||
|
||||
# Project
|
||||
rm -rf .claude/skills/my-skill
|
||||
git commit -m "Remove unused Skill"
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
### Keep Skills focused
|
||||
|
||||
One Skill should address one capability:
|
||||
|
||||
**Focused**:
|
||||
|
||||
* "PDF form filling"
|
||||
* "Excel data analysis"
|
||||
* "Git commit messages"
|
||||
|
||||
**Too broad**:
|
||||
|
||||
* "Document processing" (split into separate Skills)
|
||||
* "Data tools" (split by data type or operation)
|
||||
|
||||
### Write clear descriptions
|
||||
|
||||
Help Claude discover when to use Skills by including specific triggers in your description:
|
||||
|
||||
**Clear**:
|
||||
|
||||
```yaml theme={null}
|
||||
description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or analyzing tabular data in .xlsx format.
|
||||
```
|
||||
|
||||
**Vague**:
|
||||
|
||||
```yaml theme={null}
|
||||
description: For files
|
||||
```
|
||||
|
||||
### Test with your team
|
||||
|
||||
Have teammates use Skills and provide feedback:
|
||||
|
||||
* Does the Skill activate when expected?
|
||||
* Are the instructions clear?
|
||||
* Are there missing examples or edge cases?
|
||||
|
||||
### Document Skill versions
|
||||
|
||||
You can document Skill versions in your SKILL.md content to track changes over time. Add a version history section:
|
||||
|
||||
```markdown theme={null}
|
||||
# My Skill
|
||||
|
||||
## Version History
|
||||
- v2.0.0 (2025-10-01): Breaking changes to API
|
||||
- v1.1.0 (2025-09-15): Added new features
|
||||
- v1.0.0 (2025-09-01): Initial release
|
||||
```
|
||||
|
||||
This helps team members understand what changed between versions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Claude doesn't use my Skill
|
||||
|
||||
**Symptom**: You ask a relevant question but Claude doesn't use your Skill.
|
||||
|
||||
**Check**: Is the description specific enough?
|
||||
|
||||
Vague descriptions make discovery difficult. Include both what the Skill does and when to use it, with key terms users would mention.
|
||||
|
||||
**Too generic**:
|
||||
|
||||
```yaml theme={null}
|
||||
description: Helps with data
|
||||
```
|
||||
|
||||
**Specific**:
|
||||
|
||||
```yaml theme={null}
|
||||
description: Analyze Excel spreadsheets, generate pivot tables, create charts. Use when working with Excel files, spreadsheets, or .xlsx files.
|
||||
```
|
||||
|
||||
**Check**: Is the YAML valid?
|
||||
|
||||
Run validation to check for syntax errors:
|
||||
|
||||
```bash theme={null}
|
||||
# View frontmatter
|
||||
cat .claude/skills/my-skill/SKILL.md | head -n 15
|
||||
|
||||
# Check for common issues
|
||||
# - Missing opening or closing ---
|
||||
# - Tabs instead of spaces
|
||||
# - Unquoted strings with special characters
|
||||
```
|
||||
|
||||
**Check**: Is the Skill in the correct location?
|
||||
|
||||
```bash theme={null}
|
||||
# Personal Skills
|
||||
ls ~/.claude/skills/*/SKILL.md
|
||||
|
||||
# Project Skills
|
||||
ls .claude/skills/*/SKILL.md
|
||||
```
|
||||
|
||||
### Skill has errors
|
||||
|
||||
**Symptom**: The Skill loads but doesn't work correctly.
|
||||
|
||||
**Check**: Are dependencies available?
|
||||
|
||||
Claude will automatically install required dependencies (or ask for permission to install them) when it needs them.
|
||||
|
||||
**Check**: Do scripts have execute permissions?
|
||||
|
||||
```bash theme={null}
|
||||
chmod +x .claude/skills/my-skill/scripts/*.py
|
||||
```
|
||||
|
||||
**Check**: Are file paths correct?
|
||||
|
||||
Use forward slashes (Unix style) in all paths:
|
||||
|
||||
**Correct**: `scripts/helper.py`
|
||||
**Wrong**: `scripts\helper.py` (Windows style)
|
||||
|
||||
### Multiple Skills conflict
|
||||
|
||||
**Symptom**: Claude uses the wrong Skill or seems confused between similar Skills.
|
||||
|
||||
**Be specific in descriptions**: Help Claude choose the right Skill by using distinct trigger terms in your descriptions.
|
||||
|
||||
Instead of:
|
||||
|
||||
```yaml theme={null}
|
||||
# Skill 1
|
||||
description: For data analysis
|
||||
|
||||
# Skill 2
|
||||
description: For analyzing data
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```yaml theme={null}
|
||||
# Skill 1
|
||||
description: Analyze sales data in Excel files and CRM exports. Use for sales reports, pipeline analysis, and revenue tracking.
|
||||
|
||||
# Skill 2
|
||||
description: Analyze log files and system metrics data. Use for performance monitoring, debugging, and system diagnostics.
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Skill (single file)
|
||||
|
||||
```
|
||||
commit-helper/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
```yaml theme={null}
|
||||
---
|
||||
name: generating-commit-messages
|
||||
description: Generates clear commit messages from git diffs. Use when writing commit messages or reviewing staged changes.
|
||||
---
|
||||
|
||||
# Generating Commit Messages
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Run `git diff --staged` to see changes
|
||||
2. I'll suggest a commit message with:
|
||||
- Summary under 50 characters
|
||||
- Detailed description
|
||||
- Affected components
|
||||
|
||||
## Best practices
|
||||
|
||||
- Use present tense
|
||||
- Explain what and why, not how
|
||||
```
|
||||
|
||||
### Skill with tool permissions
|
||||
|
||||
```
|
||||
code-reviewer/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
```yaml theme={null}
|
||||
---
|
||||
name: code-reviewer
|
||||
description: Review code for best practices and potential issues. Use when reviewing code, checking PRs, or analyzing code quality.
|
||||
allowed-tools: Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
## Review checklist
|
||||
|
||||
1. Code organization and structure
|
||||
2. Error handling
|
||||
3. Performance considerations
|
||||
4. Security concerns
|
||||
5. Test coverage
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Read the target files using Read tool
|
||||
2. Search for patterns using Grep
|
||||
3. Find related files using Glob
|
||||
4. Provide detailed feedback on code quality
|
||||
```
|
||||
|
||||
### Multi-file Skill
|
||||
|
||||
```
|
||||
pdf-processing/
|
||||
├── SKILL.md
|
||||
├── FORMS.md
|
||||
├── REFERENCE.md
|
||||
└── scripts/
|
||||
├── fill_form.py
|
||||
└── validate.py
|
||||
```
|
||||
|
||||
**SKILL.md**:
|
||||
|
||||
````yaml theme={null}
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text, fill forms, merge PDFs. Use when working with PDF files, forms, or document extraction. Requires pypdf and pdfplumber packages.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Extract text:
|
||||
```python
|
||||
import pdfplumber
|
||||
with pdfplumber.open("doc.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
|
||||
For form filling, see [FORMS.md](FORMS.md).
|
||||
For detailed API reference, see [REFERENCE.md](REFERENCE.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
Packages must be installed in your environment:
|
||||
```bash
|
||||
pip install pypdf pdfplumber
|
||||
```
|
||||
````
|
||||
|
||||
<Note>
|
||||
List required packages in the description. Packages must be installed in your environment before Claude can use them.
|
||||
</Note>
|
||||
|
||||
Claude loads additional files only when needed.
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Authoring best practices" icon="lightbulb" href="/en/docs/agents-and-tools/agent-skills/best-practices">
|
||||
Write Skills that Claude can use effectively
|
||||
</Card>
|
||||
|
||||
<Card title="Agent Skills overview" icon="book" href="/en/docs/agents-and-tools/agent-skills/overview">
|
||||
Learn how Skills work across Claude products
|
||||
</Card>
|
||||
|
||||
<Card title="Use Skills in the Agent SDK" icon="cube" href="/en/api/agent-sdk/skills">
|
||||
Use Skills programmatically with TypeScript and Python
|
||||
</Card>
|
||||
|
||||
<Card title="Get started with Agent Skills" icon="rocket" href="/en/docs/agents-and-tools/agent-skills/quickstart">
|
||||
Create your first Skill
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,801 @@
|
||||
# Architecture Evolution: The Journey from v3 to v4
|
||||
|
||||
## The Problem We Solved
|
||||
|
||||
**Goal:** Create a memory system that makes Claude smarter across sessions without the user noticing it exists.
|
||||
|
||||
**Challenge:** How do you observe AI agent behavior, compress it intelligently, and serve it back at the right time - all without slowing down or interfering with the main workflow?
|
||||
|
||||
This is the story of how claude-mem evolved from a simple idea to a production-ready system, and the key architectural decisions that made it work.
|
||||
|
||||
---
|
||||
|
||||
## v1-v2: The Naive Approach
|
||||
|
||||
### The First Attempt: Dump Everything
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
PostToolUse Hook → Save raw tool outputs → Retrieve everything on startup
|
||||
```
|
||||
|
||||
**What we learned:**
|
||||
- ❌ Context pollution (thousands of tokens of irrelevant data)
|
||||
- ❌ No compression (raw tool outputs are verbose)
|
||||
- ❌ No search (had to scan everything linearly)
|
||||
- ✅ Proved the concept: Memory across sessions is valuable
|
||||
|
||||
**Example of what went wrong:**
|
||||
```
|
||||
SessionStart loaded:
|
||||
- 150 file read operations
|
||||
- 80 grep searches
|
||||
- 45 bash commands
|
||||
- Total: ~35,000 tokens
|
||||
- Relevant to current task: ~500 tokens (1.4%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v3: Smart Compression, Wrong Architecture
|
||||
|
||||
### The Breakthrough: AI-Powered Compression
|
||||
|
||||
**New idea:** Use Claude itself to compress observations
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
PostToolUse Hook → Queue observation → SDK Worker → AI compression → Store insights
|
||||
```
|
||||
|
||||
**What we added:**
|
||||
1. **Claude Agent SDK integration** - Use AI to compress observations
|
||||
2. **Background worker** - Don't block main session
|
||||
3. **Structured observations** - Extract facts, decisions, insights
|
||||
4. **Session summaries** - Generate comprehensive summaries
|
||||
|
||||
**What worked:**
|
||||
- ✅ Compression ratio: 10:1 to 100:1
|
||||
- ✅ Semantic understanding (not just keyword matching)
|
||||
- ✅ Background processing (hooks stayed fast)
|
||||
- ✅ Search became useful
|
||||
|
||||
**What didn't work:**
|
||||
- ❌ Still loaded everything upfront
|
||||
- ❌ Session ID management was broken
|
||||
- ❌ Aggressive cleanup interrupted summaries
|
||||
- ❌ Multiple SDK sessions per Claude Code session
|
||||
|
||||
---
|
||||
|
||||
## The Key Realizations
|
||||
|
||||
### Realization 1: Progressive Disclosure
|
||||
|
||||
**Problem:** Even compressed observations can pollute context if you load them all.
|
||||
|
||||
**Insight:** Humans don't read everything before starting work. Why should AI?
|
||||
|
||||
**Solution:** Show an index first, fetch details on-demand.
|
||||
|
||||
```
|
||||
❌ Old: Load 50 observations (8,500 tokens)
|
||||
✅ New: Show index of 50 observations (800 tokens)
|
||||
Agent fetches 2-3 relevant ones (300 tokens)
|
||||
Total: 1,100 tokens vs 8,500 tokens
|
||||
```
|
||||
|
||||
**Impact:**
|
||||
- 87% reduction in context usage
|
||||
- 100% relevance (only fetch what's needed)
|
||||
- Agent autonomy (decides what's relevant)
|
||||
|
||||
### Realization 2: Session ID Chaos
|
||||
|
||||
**Problem:** SDK session IDs change on every turn.
|
||||
|
||||
**What we thought:**
|
||||
```typescript
|
||||
// ❌ Wrong assumption
|
||||
UserPromptSubmit → Capture session ID once → Use forever
|
||||
```
|
||||
|
||||
**Reality:**
|
||||
```typescript
|
||||
// ✅ Actual behavior
|
||||
Turn 1: session_abc123
|
||||
Turn 2: session_def456
|
||||
Turn 3: session_ghi789
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Can't resume sessions without tracking ID updates
|
||||
- Session state gets lost between turns
|
||||
- Observations get orphaned
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Capture from system init message
|
||||
for await (const msg of response) {
|
||||
if (msg.type === 'system' && msg.subtype === 'init') {
|
||||
sdkSessionId = msg.session_id;
|
||||
await updateSessionId(sessionId, sdkSessionId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Realization 3: Graceful vs Aggressive Cleanup
|
||||
|
||||
**v3 approach:**
|
||||
```typescript
|
||||
// ❌ Aggressive: Kill worker immediately
|
||||
SessionEnd → DELETE /worker/session → Worker stops
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Summary generation interrupted mid-process
|
||||
- Pending observations lost
|
||||
- Race conditions everywhere
|
||||
|
||||
**v4 approach:**
|
||||
```typescript
|
||||
// ✅ Graceful: Let worker finish
|
||||
SessionEnd → Mark session complete → Worker finishes → Exit naturally
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Summaries complete successfully
|
||||
- No lost observations
|
||||
- Clean state transitions
|
||||
|
||||
**Code:**
|
||||
```typescript
|
||||
// v3: Aggressive
|
||||
async function sessionEnd(sessionId: string) {
|
||||
await fetch(`http://localhost:37777/sessions/${sessionId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// v4: Graceful
|
||||
async function sessionEnd(sessionId: string) {
|
||||
await db.run(
|
||||
'UPDATE sdk_sessions SET completed_at = ? WHERE id = ?',
|
||||
[Date.now(), sessionId]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Realization 4: One Session, Not Many
|
||||
|
||||
**Problem:** We were creating multiple SDK sessions per Claude Code session.
|
||||
|
||||
**What we thought:**
|
||||
```
|
||||
Claude Code session → Create SDK session per observation → 100+ SDK sessions
|
||||
```
|
||||
|
||||
**Reality should be:**
|
||||
```
|
||||
Claude Code session → ONE long-running SDK session → Streaming input
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- SDK maintains conversation state
|
||||
- Context accumulates naturally
|
||||
- Much more efficient
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// ✅ Streaming Input Mode
|
||||
async function* messageGenerator(): AsyncIterable<UserMessage> {
|
||||
// Initial prompt
|
||||
yield {
|
||||
role: "user",
|
||||
content: "You are a memory assistant..."
|
||||
};
|
||||
|
||||
// Then continuously yield observations
|
||||
while (session.status === 'active') {
|
||||
const observations = await pollQueue();
|
||||
for (const obs of observations) {
|
||||
yield {
|
||||
role: "user",
|
||||
content: formatObservation(obs)
|
||||
};
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
const response = query({
|
||||
prompt: messageGenerator(),
|
||||
options: { maxTurns: 1000 }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v4: The Architecture That Works
|
||||
|
||||
### The Core Design
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLAUDE CODE SESSION │
|
||||
│ User → Claude → Tools (Read, Edit, Write, Bash) │
|
||||
│ ↓ │
|
||||
│ PostToolUse Hook │
|
||||
│ (queues observation) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ SQLite queue
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ SDK WORKER PROCESS │
|
||||
│ ONE streaming session per Claude Code session │
|
||||
│ │
|
||||
│ AsyncIterable<UserMessage> │
|
||||
│ → Yields observations from queue │
|
||||
│ → SDK compresses via AI │
|
||||
│ → Parses XML responses │
|
||||
│ → Stores in database │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ SQLite storage
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ NEXT SESSION │
|
||||
│ SessionStart Hook │
|
||||
│ → Queries database │
|
||||
│ → Returns progressive disclosure index │
|
||||
│ → Agent fetches details via MCP │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### The Five Hook Architecture
|
||||
|
||||
<Tabs>
|
||||
<Tab title="SessionStart">
|
||||
**Purpose:** Inject context from previous sessions
|
||||
|
||||
**Timing:** When Claude Code starts
|
||||
|
||||
**What it does:**
|
||||
- Queries last 10 session summaries
|
||||
- Formats as progressive disclosure index
|
||||
- Injects into context via stdout
|
||||
|
||||
**Key change from v3:**
|
||||
- ✅ Index format (not full details)
|
||||
- ✅ Token counts visible
|
||||
- ✅ MCP search instructions included
|
||||
</Tab>
|
||||
|
||||
<Tab title="UserPromptSubmit">
|
||||
**Purpose:** Initialize session tracking
|
||||
|
||||
**Timing:** Before Claude processes prompt
|
||||
|
||||
**What it does:**
|
||||
- Creates session record
|
||||
- Saves raw user prompt (v4.2.0+)
|
||||
- Starts worker if needed
|
||||
|
||||
**Key change from v3:**
|
||||
- ✅ Stores raw prompts for search
|
||||
- ✅ Auto-starts PM2 worker
|
||||
</Tab>
|
||||
|
||||
<Tab title="PostToolUse">
|
||||
**Purpose:** Capture tool observations
|
||||
|
||||
**Timing:** After every tool execution
|
||||
|
||||
**What it does:**
|
||||
- Enqueues observation in database
|
||||
- Returns immediately
|
||||
|
||||
**Key change from v3:**
|
||||
- ✅ Just enqueues (doesn't process)
|
||||
- ✅ Worker handles all AI calls
|
||||
</Tab>
|
||||
|
||||
<Tab title="Summary">
|
||||
**Purpose:** Generate session summaries
|
||||
|
||||
**Timing:** Worker-triggered (mid-session)
|
||||
|
||||
**What it does:**
|
||||
- Gathers observations
|
||||
- Sends to Claude for summarization
|
||||
- Stores structured summary
|
||||
|
||||
**Key change from v3:**
|
||||
- ✅ Multiple summaries per session
|
||||
- ✅ Summaries are checkpoints, not endings
|
||||
</Tab>
|
||||
|
||||
<Tab title="SessionEnd">
|
||||
**Purpose:** Graceful cleanup
|
||||
|
||||
**Timing:** When session ends
|
||||
|
||||
**What it does:**
|
||||
- Marks session complete
|
||||
- Lets worker finish processing
|
||||
|
||||
**Key change from v3:**
|
||||
- ✅ Graceful (not aggressive)
|
||||
- ✅ No DELETE requests
|
||||
- ✅ Worker finishes naturally
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Database Schema Evolution
|
||||
|
||||
**v3 schema:**
|
||||
```sql
|
||||
-- Simple, flat structure
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
session_id TEXT,
|
||||
text TEXT,
|
||||
created_at INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
**v4 schema:**
|
||||
```sql
|
||||
-- Rich, structured schema
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
|
||||
-- Progressive disclosure metadata
|
||||
title TEXT NOT NULL,
|
||||
subtitle TEXT,
|
||||
type TEXT NOT NULL, -- decision, bugfix, feature, etc.
|
||||
|
||||
-- Content
|
||||
narrative TEXT NOT NULL,
|
||||
facts TEXT, -- JSON array
|
||||
|
||||
-- Searchability
|
||||
concepts TEXT, -- JSON array of tags
|
||||
files_read TEXT, -- JSON array
|
||||
files_modified TEXT, -- JSON array
|
||||
|
||||
-- Timestamps
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY(session_id) REFERENCES sdk_sessions(id)
|
||||
);
|
||||
|
||||
-- FTS5 for full-text search
|
||||
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
||||
title, subtitle, narrative, facts, concepts,
|
||||
content=observations
|
||||
);
|
||||
|
||||
-- Auto-sync triggers
|
||||
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
|
||||
END;
|
||||
```
|
||||
|
||||
**What changed:**
|
||||
- ✅ Structured fields (title, subtitle, type)
|
||||
- ✅ FTS5 full-text search
|
||||
- ✅ Project-scoped queries
|
||||
- ✅ Rich metadata for progressive disclosure
|
||||
|
||||
### Worker Service Redesign
|
||||
|
||||
**v3 worker:**
|
||||
```typescript
|
||||
// Multiple short SDK sessions
|
||||
app.post('/process', async (req, res) => {
|
||||
const response = await query({
|
||||
prompt: buildPrompt(req.body),
|
||||
options: { maxTurns: 1 }
|
||||
});
|
||||
|
||||
for await (const msg of response) {
|
||||
// Process single observation
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
**v4 worker:**
|
||||
```typescript
|
||||
// ONE long-running SDK session
|
||||
async function runWorker(sessionId: string) {
|
||||
const response = query({
|
||||
prompt: messageGenerator(), // AsyncIterable
|
||||
options: { maxTurns: 1000 }
|
||||
});
|
||||
|
||||
for await (const msg of response) {
|
||||
if (msg.type === 'text') {
|
||||
parseObservations(msg.content);
|
||||
parseSummaries(msg.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Maintains conversation state
|
||||
- SDK handles context automatically
|
||||
- More efficient (fewer API calls)
|
||||
- Natural multi-turn flow
|
||||
|
||||
---
|
||||
|
||||
## Critical Fixes Along the Way
|
||||
|
||||
### Fix 1: Context Injection Pollution (v4.3.1)
|
||||
|
||||
**Problem:** SessionStart hook output polluted with npm install logs
|
||||
|
||||
```bash
|
||||
# Hook output contained:
|
||||
npm WARN deprecated ...
|
||||
npm WARN deprecated ...
|
||||
{"hookSpecificOutput": {"additionalContext": "..."}}
|
||||
```
|
||||
|
||||
**Why it broke:**
|
||||
- Claude Code expects clean JSON or plain text
|
||||
- stderr/stdout from npm install mixed with hook output
|
||||
- Context didn't inject properly
|
||||
|
||||
**Solution:**
|
||||
```json
|
||||
{
|
||||
"command": "npm install --loglevel=silent && node context-hook.js"
|
||||
}
|
||||
```
|
||||
|
||||
**Result:** Clean JSON output, context injection works
|
||||
|
||||
### Fix 2: Double Shebang Issue (v4.3.1)
|
||||
|
||||
**Problem:** Hook executables had duplicate shebangs
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env node // ← Duplicate!
|
||||
|
||||
// Rest of code...
|
||||
```
|
||||
|
||||
**Why it happened:**
|
||||
- Source files had shebang
|
||||
- esbuild added another shebang during build
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Remove shebangs from source files
|
||||
// Let esbuild add them during build
|
||||
```
|
||||
|
||||
**Result:** Clean executables, no parsing errors
|
||||
|
||||
### Fix 3: FTS5 Injection Vulnerability (v4.2.3)
|
||||
|
||||
**Problem:** User input passed directly to FTS5 query
|
||||
|
||||
```typescript
|
||||
// ❌ Vulnerable
|
||||
const results = db.query(
|
||||
`SELECT * FROM observations_fts WHERE observations_fts MATCH '${userQuery}'`
|
||||
);
|
||||
```
|
||||
|
||||
**Attack:**
|
||||
```typescript
|
||||
userQuery = "'; DROP TABLE observations; --"
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// ✅ Safe: Use parameterized queries
|
||||
const results = db.query(
|
||||
'SELECT * FROM observations_fts WHERE observations_fts MATCH ?',
|
||||
[userQuery]
|
||||
);
|
||||
```
|
||||
|
||||
### Fix 4: NOT NULL Constraint Violation (v4.2.8)
|
||||
|
||||
**Problem:** Session creation failed when prompt was empty
|
||||
|
||||
```sql
|
||||
INSERT INTO sdk_sessions (claude_session_id, user_prompt, ...)
|
||||
VALUES ('abc123', NULL, ...) -- ❌ user_prompt is NOT NULL
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Allow NULL user_prompts
|
||||
user_prompt: input.prompt ?? null
|
||||
```
|
||||
|
||||
**Schema change:**
|
||||
```sql
|
||||
-- Before
|
||||
user_prompt TEXT NOT NULL
|
||||
|
||||
-- After
|
||||
user_prompt TEXT -- Nullable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
### Optimization 1: Prepared Statements
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
for (const obs of observations) {
|
||||
db.run(`INSERT INTO observations (...) VALUES (?, ?, ...)`, [obs.id, obs.text, ...]);
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
const stmt = db.prepare(`INSERT INTO observations (...) VALUES (?, ?, ...)`);
|
||||
for (const obs of observations) {
|
||||
stmt.run([obs.id, obs.text, ...]);
|
||||
}
|
||||
stmt.finalize();
|
||||
```
|
||||
|
||||
**Impact:** 5x faster bulk inserts
|
||||
|
||||
### Optimization 2: FTS5 Indexing
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
// Manual full-text search
|
||||
const results = db.query(
|
||||
`SELECT * FROM observations WHERE text LIKE '%${query}%'`
|
||||
);
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
// FTS5 virtual table
|
||||
const results = db.query(
|
||||
`SELECT * FROM observations_fts WHERE observations_fts MATCH ?`,
|
||||
[query]
|
||||
);
|
||||
```
|
||||
|
||||
**Impact:** 100x faster searches on large datasets
|
||||
|
||||
### Optimization 3: Index Format Default
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
// Always return full observations
|
||||
search_observations({ query: "hooks" });
|
||||
// Returns: 5,000 tokens
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
// Default to index format
|
||||
search_observations({ query: "hooks", format: "index" });
|
||||
// Returns: 200 tokens
|
||||
|
||||
// Fetch full only when needed
|
||||
search_observations({ query: "hooks", format: "full", limit: 1 });
|
||||
// Returns: 150 tokens
|
||||
```
|
||||
|
||||
**Impact:** 25x reduction in average search result size
|
||||
|
||||
---
|
||||
|
||||
## What We Learned
|
||||
|
||||
### Lesson 1: Context is Precious
|
||||
|
||||
**Principle:** Every token you put in context window costs attention.
|
||||
|
||||
**Application:**
|
||||
- Progressive disclosure reduces waste by 87%
|
||||
- Index-first approach gives agent control
|
||||
- Token counts make costs visible
|
||||
|
||||
### Lesson 2: Session State is Complicated
|
||||
|
||||
**Principle:** Distributed state is hard. SDK handles it better than we can.
|
||||
|
||||
**Application:**
|
||||
- Use SDK's built-in session resumption
|
||||
- Don't try to manually reconstruct state
|
||||
- Track session IDs from init messages
|
||||
|
||||
### Lesson 3: Graceful Beats Aggressive
|
||||
|
||||
**Principle:** Let processes finish their work before terminating.
|
||||
|
||||
**Application:**
|
||||
- Graceful cleanup prevents data loss
|
||||
- Workers finish important operations
|
||||
- Clean state transitions reduce bugs
|
||||
|
||||
### Lesson 4: AI is the Compressor
|
||||
|
||||
**Principle:** Don't compress manually. Let AI do semantic compression.
|
||||
|
||||
**Application:**
|
||||
- 10:1 to 100:1 compression ratios
|
||||
- Semantic understanding, not keyword extraction
|
||||
- Structured outputs (XML parsing)
|
||||
|
||||
### Lesson 5: Progressive Everything
|
||||
|
||||
**Principle:** Show metadata first, fetch details on-demand.
|
||||
|
||||
**Application:**
|
||||
- Progressive disclosure in context injection
|
||||
- Index format in search results
|
||||
- Layer 1 (titles) → Layer 2 (summaries) → Layer 3 (full details)
|
||||
|
||||
---
|
||||
|
||||
## The Road Ahead
|
||||
|
||||
### Planned: Adaptive Index Size
|
||||
|
||||
```typescript
|
||||
SessionStart({ source: "startup" }):
|
||||
→ Show last 10 sessions (normal)
|
||||
|
||||
SessionStart({ source: "resume" }):
|
||||
→ Show only current session (minimal)
|
||||
|
||||
SessionStart({ source: "compact" }):
|
||||
→ Show last 20 sessions (comprehensive)
|
||||
```
|
||||
|
||||
### Planned: Relevance Scoring
|
||||
|
||||
```typescript
|
||||
// Use embeddings to pre-sort index by semantic relevance
|
||||
search_observations({
|
||||
query: "authentication bug",
|
||||
sort: "relevance" // Based on embeddings
|
||||
});
|
||||
```
|
||||
|
||||
### Planned: Multi-Project Context
|
||||
|
||||
```typescript
|
||||
// Cross-project pattern recognition
|
||||
search_observations({
|
||||
query: "API rate limiting",
|
||||
projects: ["api-gateway", "user-service", "billing-service"]
|
||||
});
|
||||
```
|
||||
|
||||
### Planned: Collaborative Memory
|
||||
|
||||
```typescript
|
||||
// Team-shared observations (optional)
|
||||
createObservation({
|
||||
title: "Rate limit: 100 req/min",
|
||||
scope: "team" // vs "user"
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide: v3 → v4
|
||||
|
||||
### Step 1: Backup Database
|
||||
|
||||
```bash
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem-v3-backup.db
|
||||
```
|
||||
|
||||
### Step 2: Update Plugin
|
||||
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
git pull
|
||||
```
|
||||
|
||||
### Step 3: Run Migration
|
||||
|
||||
```bash
|
||||
npx tsx src/services/sqlite/migrations/v3-to-v4.ts
|
||||
```
|
||||
|
||||
**What the migration does:**
|
||||
- Adds new columns to observations table
|
||||
- Creates FTS5 virtual tables
|
||||
- Sets up auto-sync triggers
|
||||
- Migrates existing observations to new schema
|
||||
|
||||
### Step 4: Restart Worker
|
||||
|
||||
```bash
|
||||
pm2 restart claude-mem-worker
|
||||
pm2 logs claude-mem-worker
|
||||
```
|
||||
|
||||
### Step 5: Test
|
||||
|
||||
```bash
|
||||
# Start Claude Code
|
||||
claude
|
||||
|
||||
# Check that context is injected
|
||||
# (Should see progressive disclosure index)
|
||||
|
||||
# Submit a prompt and check observations
|
||||
pm2 logs claude-mem-worker --nostream
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Metrics
|
||||
|
||||
### v3 Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Context usage per session | ~25,000 tokens |
|
||||
| Relevant context | ~2,000 tokens (8%) |
|
||||
| Hook execution time | ~200ms |
|
||||
| Search latency | ~500ms (LIKE queries) |
|
||||
|
||||
### v4 Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Context usage per session | ~1,100 tokens |
|
||||
| Relevant context | ~1,100 tokens (100%) |
|
||||
| Hook execution time | ~45ms |
|
||||
| Search latency | ~15ms (FTS5) |
|
||||
|
||||
**Improvements:**
|
||||
- 96% reduction in context waste
|
||||
- 12x increase in relevance
|
||||
- 4x faster hooks
|
||||
- 33x faster search
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The journey from v3 to v4 was about understanding these fundamental truths:
|
||||
|
||||
1. **Context is finite** - Progressive disclosure respects attention budget
|
||||
2. **AI is the compressor** - Semantic understanding beats keyword extraction
|
||||
3. **Agents are smart** - Let them decide what to fetch
|
||||
4. **State is hard** - Use SDK's built-in mechanisms
|
||||
5. **Graceful wins** - Let processes finish cleanly
|
||||
|
||||
The result is a memory system that's both powerful and invisible. Users never notice it working - Claude just gets smarter over time.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Progressive Disclosure](/docs/progressive-disclosure) - The philosophy behind v4
|
||||
- [Hooks Architecture](/docs/hooks-architecture) - How hooks power the system
|
||||
- [Context Engineering](/docs/context-engineering) - Foundational principles
|
||||
- [v4.0.0 Release Notes](/CHANGELOG.md#v400) - Full changelog
|
||||
|
||||
---
|
||||
|
||||
*This architecture evolution reflects hundreds of hours of experimentation, dozens of dead ends, and the invaluable experience of real-world usage. v4 is the architecture that emerged from understanding what actually works.*
|
||||
@@ -28,7 +28,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=silent && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
@@ -91,7 +91,9 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/context.ts` and `src/bin/hooks/context-hook.ts`
|
||||
**Implementation**: `src/hooks/context-hook.ts`
|
||||
|
||||
**v4.3.1 Fix**: Changed npm install to use `--loglevel=silent` instead of `--loglevel=error` to prevent output pollution that was breaking JSON context injection.
|
||||
|
||||
## 2. UserPromptSubmit Hook (`new-hook.js`)
|
||||
|
||||
@@ -112,7 +114,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/new.ts` and `src/bin/hooks/new-hook.ts`
|
||||
**Implementation**: `src/hooks/new-hook.ts`
|
||||
|
||||
## 3. PostToolUse Hook (`save-hook.js`)
|
||||
|
||||
@@ -136,7 +138,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/save.ts` and `src/bin/hooks/save-hook.ts`
|
||||
**Implementation**: `src/hooks/save-hook.ts`
|
||||
|
||||
## 4. Stop Hook (`summary-hook.js`)
|
||||
|
||||
@@ -156,7 +158,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/summary.ts` and `src/bin/hooks/summary-hook.ts`
|
||||
**Implementation**: `src/hooks/summary-hook.ts`
|
||||
|
||||
## 5. SessionEnd Hook (`cleanup-hook.js`)
|
||||
|
||||
@@ -177,16 +179,15 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/cleanup.ts` and `src/bin/hooks/cleanup-hook.ts`
|
||||
**Implementation**: `src/hooks/cleanup-hook.ts`
|
||||
|
||||
## Hook Development
|
||||
|
||||
### Adding a New Hook
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`
|
||||
3. Add to `plugin/hooks/hooks.json`
|
||||
4. Rebuild with `npm run build`
|
||||
2. Add to `plugin/hooks/hooks.json`
|
||||
3. Rebuild with `npm run build`
|
||||
|
||||
### Hook Best Practices
|
||||
|
||||
|
||||
@@ -90,20 +90,13 @@ Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Se
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── bin/hooks/ # Entry point scripts for 5 hooks
|
||||
│ ├── hooks/ # Hook implementations (v4.3.1+ consolidated)
|
||||
│ │ ├── context-hook.ts # SessionStart
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit
|
||||
│ │ ├── save-hook.ts # PostToolUse
|
||||
│ │ ├── summary-hook.ts # Stop
|
||||
│ │ └── cleanup-hook.ts # SessionEnd
|
||||
│ │
|
||||
│ ├── hooks/ # Hook implementation logic
|
||||
│ │ ├── context.ts
|
||||
│ │ ├── new.ts
|
||||
│ │ ├── save.ts
|
||||
│ │ ├── summary.ts
|
||||
│ │ └── cleanup.ts
|
||||
│ │
|
||||
│ ├── servers/ # MCP servers
|
||||
│ │ └── search-server.ts # MCP search tools server
|
||||
│ │
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# Context Engineering for AI Agents: Best Practices Cheat Sheet
|
||||
|
||||
## Core Principle
|
||||
**Find the smallest possible set of high-signal tokens that maximize the likelihood of your desired outcome.**
|
||||
|
||||
---
|
||||
|
||||
## Context Engineering vs Prompt Engineering
|
||||
|
||||
**Prompt Engineering**: Writing and organizing LLM instructions for optimal outcomes (one-time task)
|
||||
|
||||
**Context Engineering**: Curating and maintaining the optimal set of tokens during inference across multiple turns (iterative process)
|
||||
|
||||
Context engineering manages:
|
||||
- System instructions
|
||||
- Tools
|
||||
- Model Context Protocol (MCP)
|
||||
- External data
|
||||
- Message history
|
||||
- Runtime data retrieval
|
||||
|
||||
---
|
||||
|
||||
## The Problem: Context Rot
|
||||
|
||||
**Key Insight**: LLMs have an "attention budget" that gets depleted as context grows
|
||||
|
||||
- Every token attends to every other token (n² relationships)
|
||||
- As context length increases, model accuracy decreases
|
||||
- Models have less training experience with longer sequences
|
||||
- Context must be treated as a finite resource with diminishing marginal returns
|
||||
|
||||
---
|
||||
|
||||
## System Prompts: Find the "Right Altitude"
|
||||
|
||||
### The Goldilocks Zone
|
||||
|
||||
**Too Prescriptive** ❌
|
||||
- Hardcoded if-else logic
|
||||
- Brittle and fragile
|
||||
- High maintenance complexity
|
||||
|
||||
**Too Vague** ❌
|
||||
- High-level guidance without concrete signals
|
||||
- Falsely assumes shared context
|
||||
- Lacks actionable direction
|
||||
|
||||
**Just Right** ✅
|
||||
- Specific enough to guide behavior effectively
|
||||
- Flexible enough to provide strong heuristics
|
||||
- Minimal set of information that fully outlines expected behavior
|
||||
|
||||
### Best Practices
|
||||
- Use simple, direct language
|
||||
- Organize into distinct sections (`<background_information>`, `<instructions>`, `## Tool guidance`, etc.)
|
||||
- Use XML tags or Markdown headers for structure
|
||||
- Start with minimal prompt, add based on failure modes
|
||||
- Note: Minimal ≠ short (provide sufficient information upfront)
|
||||
|
||||
---
|
||||
|
||||
## Tools: Minimal and Clear
|
||||
|
||||
### Design Principles
|
||||
- **Self-contained**: Each tool has a single, clear purpose
|
||||
- **Robust to error**: Handle edge cases gracefully
|
||||
- **Extremely clear**: Intended use is unambiguous
|
||||
- **Token-efficient**: Returns relevant information without bloat
|
||||
- **Descriptive parameters**: Unambiguous input names (e.g., `user_id` not `user`)
|
||||
|
||||
### Critical Rule
|
||||
**If a human engineer can't definitively say which tool to use in a given situation, an AI agent can't be expected to do better.**
|
||||
|
||||
### Common Failure Modes to Avoid
|
||||
- Bloated tool sets covering too much functionality
|
||||
- Tools with overlapping purposes
|
||||
- Ambiguous decision points about which tool to use
|
||||
|
||||
---
|
||||
|
||||
## Examples: Diverse, Not Exhaustive
|
||||
|
||||
**Do** ✅
|
||||
- Curate a set of diverse, canonical examples
|
||||
- Show expected behavior effectively
|
||||
- Think "pictures worth a thousand words"
|
||||
|
||||
**Don't** ❌
|
||||
- Stuff in a laundry list of edge cases
|
||||
- Try to articulate every possible rule
|
||||
- Overwhelm with exhaustive scenarios
|
||||
|
||||
---
|
||||
|
||||
## Context Retrieval Strategies
|
||||
|
||||
### Just-In-Time Context (Recommended for Agents)
|
||||
**Approach**: Maintain lightweight identifiers (file paths, queries, links) and dynamically load data at runtime
|
||||
|
||||
**Benefits**:
|
||||
- Avoids context pollution
|
||||
- Enables progressive disclosure
|
||||
- Mirrors human cognition (we don't memorize everything)
|
||||
- Leverages metadata (file names, folder structure, timestamps)
|
||||
- Agents discover context incrementally
|
||||
|
||||
**Trade-offs**:
|
||||
- Slower than pre-computed retrieval
|
||||
- Requires proper tool guidance to avoid dead-ends
|
||||
|
||||
### Pre-Inference Retrieval (Traditional RAG)
|
||||
**Approach**: Use embedding-based retrieval to surface context before inference
|
||||
|
||||
**When to Use**: Static content that won't change during interaction
|
||||
|
||||
### Hybrid Strategy (Best of Both)
|
||||
**Approach**: Retrieve some data upfront, enable autonomous exploration as needed
|
||||
|
||||
**Example**: Claude Code loads CLAUDE.md files upfront, uses glob/grep for just-in-time retrieval
|
||||
|
||||
**Rule of Thumb**: "Do the simplest thing that works"
|
||||
|
||||
---
|
||||
|
||||
## Long-Horizon Tasks: Three Techniques
|
||||
|
||||
### 1. Compaction
|
||||
**What**: Summarize conversation nearing context limit, reinitiate with summary
|
||||
|
||||
**Implementation**:
|
||||
- Pass message history to model for compression
|
||||
- Preserve critical details (architectural decisions, bugs, implementation)
|
||||
- Discard redundant outputs
|
||||
- Continue with compressed context + recently accessed files
|
||||
|
||||
**Tuning Process**:
|
||||
1. **First**: Maximize recall (capture all relevant information)
|
||||
2. **Then**: Improve precision (eliminate superfluous content)
|
||||
|
||||
**Low-Hanging Fruit**: Clear old tool calls and results
|
||||
|
||||
**Best For**: Tasks requiring extensive back-and-forth
|
||||
|
||||
### 2. Structured Note-Taking (Agentic Memory)
|
||||
**What**: Agent writes notes persisted outside context window, retrieved later
|
||||
|
||||
**Examples**:
|
||||
- To-do lists
|
||||
- NOTES.md files
|
||||
- Game state tracking (Pokémon example: tracking 1,234 steps of training)
|
||||
- Project progress logs
|
||||
|
||||
**Benefits**:
|
||||
- Persistent memory with minimal overhead
|
||||
- Maintains critical context across tool calls
|
||||
- Enables multi-hour coherent strategies
|
||||
|
||||
**Best For**: Iterative development with clear milestones
|
||||
|
||||
### 3. Sub-Agent Architectures
|
||||
**What**: Specialized sub-agents handle focused tasks with clean context windows
|
||||
|
||||
**How It Works**:
|
||||
- Main agent coordinates high-level plan
|
||||
- Sub-agents perform deep technical work
|
||||
- Sub-agents explore extensively (tens of thousands of tokens)
|
||||
- Return condensed summaries (1,000-2,000 tokens)
|
||||
|
||||
**Benefits**:
|
||||
- Clear separation of concerns
|
||||
- Parallel exploration
|
||||
- Detailed context remains isolated
|
||||
|
||||
**Best For**: Complex research and analysis tasks
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Framework
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Static content | Pre-inference retrieval or hybrid |
|
||||
| Dynamic exploration needed | Just-in-time context |
|
||||
| Extended back-and-forth | Compaction |
|
||||
| Iterative development | Structured note-taking |
|
||||
| Complex research | Sub-agent architectures |
|
||||
| Rapid model improvement | "Do the simplest thing that works" |
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Context is finite**: Treat it as a precious resource with an attention budget
|
||||
2. **Think holistically**: Consider the entire state available to the LLM
|
||||
3. **Stay minimal**: More context isn't always better
|
||||
4. **Be iterative**: Context curation happens each time you pass to the model
|
||||
5. **Design for autonomy**: As models improve, let them act intelligently
|
||||
6. **Start simple**: Test with minimal setup, add based on failure modes
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- ❌ Cramming everything into prompts
|
||||
- ❌ Creating brittle if-else logic
|
||||
- ❌ Building bloated tool sets
|
||||
- ❌ Stuffing exhaustive edge cases as examples
|
||||
- ❌ Assuming larger context windows solve everything
|
||||
- ❌ Ignoring context pollution over long interactions
|
||||
|
||||
---
|
||||
|
||||
## Remember
|
||||
|
||||
> "Even as models continue to improve, the challenge of maintaining coherence across extended interactions will remain central to building more effective agents."
|
||||
|
||||
Context engineering will evolve, but the core principle stays the same: **optimize signal-to-noise ratio in your token budget**.
|
||||
|
||||
---
|
||||
|
||||
*Based on Anthropic's "Effective context engineering for AI agents" (September 2025)*
|
||||
+19
-23
@@ -61,8 +61,7 @@ Edit TypeScript source files in `src/`:
|
||||
|
||||
```
|
||||
src/
|
||||
├── bin/hooks/ # Hook entry points
|
||||
├── hooks/ # Hook implementations
|
||||
├── hooks/ # Hook implementations (entry points + logic)
|
||||
├── services/ # Worker service and database
|
||||
├── servers/ # MCP search server
|
||||
├── sdk/ # Claude Agent SDK integration
|
||||
@@ -118,34 +117,27 @@ Repeat steps 1-4 until your changes work as expected.
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`:
|
||||
|
||||
```typescript
|
||||
import { HookInput } from './types';
|
||||
|
||||
export async function yourHook(input: HookInput) {
|
||||
// Hook implementation
|
||||
return {
|
||||
hookSpecificOutput: 'Optional output'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`:
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
import { readStdin } from '../../shared/stdin';
|
||||
import { yourHook } from '../../hooks/your-hook';
|
||||
import { readStdin } from '../shared/stdin';
|
||||
|
||||
async function main() {
|
||||
const input = await readStdin();
|
||||
const result = await yourHook(input);
|
||||
|
||||
// Hook implementation
|
||||
const result = {
|
||||
hookSpecificOutput: 'Optional output'
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
3. Add to `plugin/hooks/hooks.json`:
|
||||
**Note**: As of v4.3.1, hooks are self-contained files. The shebang will be added automatically by esbuild during the build process.
|
||||
|
||||
2. Add to `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -424,18 +416,22 @@ The `release` script:
|
||||
6. Publish to NPM
|
||||
|
||||
```bash
|
||||
# Update version
|
||||
npm version 4.2.4
|
||||
# Use the version bump skill (recommended as of v4.3.0)
|
||||
# In Claude Code, run: /skill version-bump
|
||||
# This updates package.json, marketplace.json, and CLAUDE.md
|
||||
|
||||
# Or manually:
|
||||
npm version 4.3.2
|
||||
|
||||
# Update changelog
|
||||
# Edit CHANGELOG.md manually
|
||||
|
||||
# Commit
|
||||
git add .
|
||||
git commit -m "chore: Release v4.2.4"
|
||||
git commit -m "chore: Release v4.3.2"
|
||||
|
||||
# Tag
|
||||
git tag v4.2.4
|
||||
git tag v4.3.2
|
||||
|
||||
# Push
|
||||
git push origin main --tags
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
"usage/search-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Best Practices",
|
||||
"icon": "lightbulb",
|
||||
"pages": [
|
||||
"context-engineering",
|
||||
"progressive-disclosure"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Configuration & Development",
|
||||
"icon": "gear",
|
||||
@@ -53,6 +61,8 @@
|
||||
"icon": "diagram-project",
|
||||
"pages": [
|
||||
"architecture/overview",
|
||||
"architecture-evolution",
|
||||
"hooks-architecture",
|
||||
"architecture/hooks",
|
||||
"architecture/worker-service",
|
||||
"architecture/database",
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
# How Claude-Mem Uses Hooks: A Lifecycle-Driven Architecture
|
||||
|
||||
## Core Principle
|
||||
**Observe the main Claude Code session from the outside, process observations in the background, inject context at the right time.**
|
||||
|
||||
---
|
||||
|
||||
## The Big Picture
|
||||
|
||||
Claude-Mem is fundamentally a **hook-driven system**. Every piece of functionality happens in response to lifecycle events:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLAUDE CODE SESSION │
|
||||
│ (Main session - user interacting with Claude) │
|
||||
│ │
|
||||
│ SessionStart → UserPromptSubmit → Tool Use → Stop │
|
||||
│ ↓ ↓ ↓ ↓ │
|
||||
│ [Hook] [Hook] [Hook] [Hook] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ ↓ ↓ ↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLAUDE-MEM SYSTEM │
|
||||
│ │
|
||||
│ Context New Session Observation Summary │
|
||||
│ Injection Tracking Capture Generation │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key insight:** Claude-Mem doesn't interrupt or modify Claude Code's behavior. It observes from the outside and provides value through lifecycle hooks.
|
||||
|
||||
---
|
||||
|
||||
## Why Hooks?
|
||||
|
||||
### The Non-Invasive Requirement
|
||||
|
||||
Claude-Mem had several architectural constraints:
|
||||
|
||||
1. **Can't modify Claude Code**: It's a closed-source binary
|
||||
2. **Must be fast**: Can't slow down the main session
|
||||
3. **Must be reliable**: Can't break Claude Code if it fails
|
||||
4. **Must be portable**: Works on any project without configuration
|
||||
|
||||
**Solution:** External command hooks configured via settings.json
|
||||
|
||||
### The Hook System Advantage
|
||||
|
||||
Claude Code's hook system provides exactly what we need:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Lifecycle Events" icon="clock">
|
||||
SessionStart, UserPromptSubmit, PostToolUse, Stop
|
||||
</Card>
|
||||
|
||||
<Card title="Non-Blocking" icon="forward">
|
||||
Hooks run in parallel, don't wait for completion
|
||||
</Card>
|
||||
|
||||
<Card title="Context Injection" icon="upload">
|
||||
SessionStart and UserPromptSubmit can add context
|
||||
</Card>
|
||||
|
||||
<Card title="Tool Observation" icon="eye">
|
||||
PostToolUse sees all tool inputs and outputs
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
---
|
||||
|
||||
## The Five Hooks
|
||||
|
||||
### Hook 1: SessionStart (Context Hook)
|
||||
|
||||
**Purpose:** Inject relevant context from previous sessions
|
||||
|
||||
**When:** Claude Code starts or resumes
|
||||
|
||||
**What it does:**
|
||||
1. Extracts project name from current working directory
|
||||
2. Queries SQLite for recent session summaries (last 10)
|
||||
3. Queries SQLite for recent observations (last 50)
|
||||
4. Formats as progressive disclosure index
|
||||
5. Outputs to stdout (automatically injected into context)
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "startup",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ Only runs on "startup" (not "clear" or "compact")
|
||||
- ✅ 120-second timeout for npm install (v4.3.1 fix)
|
||||
- ✅ Uses `--loglevel=silent` for clean JSON output
|
||||
- ✅ Progressive disclosure format (index, not full details)
|
||||
|
||||
**Output format:**
|
||||
```markdown
|
||||
# [claude-mem] recent context
|
||||
|
||||
**Legend:** 🎯 session-request | 🔴 gotcha | 🟡 problem-solution ...
|
||||
|
||||
### Oct 26, 2025
|
||||
|
||||
**General**
|
||||
| ID | Time | T | Title | Tokens |
|
||||
|----|------|---|-------|--------|
|
||||
| #2586 | 12:58 AM | 🔵 | Context hook file empty | ~51 |
|
||||
|
||||
*Use claude-mem MCP search to access full details*
|
||||
```
|
||||
|
||||
**Source:** `src/hooks/context-hook.ts` → `plugin/scripts/context-hook.js`
|
||||
|
||||
---
|
||||
|
||||
### Hook 2: UserPromptSubmit (New Session Hook)
|
||||
|
||||
**Purpose:** Initialize session tracking when user submits a prompt
|
||||
|
||||
**When:** Before Claude processes the user's message
|
||||
|
||||
**What it does:**
|
||||
1. Reads user prompt and session ID from stdin
|
||||
2. Creates new session record in SQLite
|
||||
3. Saves raw user prompt for full-text search (v4.2.0+)
|
||||
4. Starts PM2 worker service if not running
|
||||
5. Returns immediately (non-blocking)
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ No matcher (runs for all prompts)
|
||||
- ✅ Creates session record immediately
|
||||
- ✅ Stores raw prompts for search (privacy note: local SQLite only)
|
||||
- ✅ Auto-starts worker service
|
||||
- ✅ Suppresses output (`suppressOutput: true`)
|
||||
|
||||
**Database operations:**
|
||||
```sql
|
||||
INSERT INTO sdk_sessions (claude_session_id, project, user_prompt, ...)
|
||||
VALUES (?, ?, ?, ...)
|
||||
|
||||
INSERT INTO user_prompts (session_id, prompt, prompt_number, ...)
|
||||
VALUES (?, ?, ?, ...)
|
||||
```
|
||||
|
||||
**Source:** `src/hooks/new-hook.ts` → `plugin/scripts/new-hook.js`
|
||||
|
||||
---
|
||||
|
||||
### Hook 3: PostToolUse (Save Observation Hook)
|
||||
|
||||
**Purpose:** Capture tool execution observations for later processing
|
||||
|
||||
**When:** Immediately after any tool completes successfully
|
||||
|
||||
**What it does:**
|
||||
1. Receives tool name, input, output from stdin
|
||||
2. Finds active session for current project
|
||||
3. Enqueues observation in observation_queue table
|
||||
4. Returns immediately (processing happens in worker)
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ Matcher: `*` (captures all tools)
|
||||
- ✅ Non-blocking (just enqueues, doesn't process)
|
||||
- ✅ Worker processes observations asynchronously
|
||||
- ✅ Parallel execution safe (each hook gets own stdin)
|
||||
|
||||
**Database operations:**
|
||||
```sql
|
||||
INSERT INTO observation_queue (session_id, tool_name, tool_input, tool_output, ...)
|
||||
VALUES (?, ?, ?, ?, ...)
|
||||
```
|
||||
|
||||
**What gets queued:**
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.ts",
|
||||
"old_string": "...",
|
||||
"new_string": "..."
|
||||
},
|
||||
"tool_output": {
|
||||
"success": true,
|
||||
"linesChanged": 5
|
||||
},
|
||||
"created_at_epoch": 1698765432
|
||||
}
|
||||
```
|
||||
|
||||
**Source:** `src/hooks/save-hook.ts` → `plugin/scripts/save-hook.js`
|
||||
|
||||
---
|
||||
|
||||
### Hook 4: Summary Hook (Mid-Session Checkpoint)
|
||||
|
||||
**Purpose:** Generate AI-powered session summaries during the session
|
||||
|
||||
**When:** Triggered programmatically by the worker service
|
||||
|
||||
**What it does:**
|
||||
1. Gathers session observations from database
|
||||
2. Sends to Claude Agent SDK for summarization
|
||||
3. Processes response and extracts structured summary
|
||||
4. Stores in session_summaries table
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Summary": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ Triggered by worker, not by Claude Code lifecycle
|
||||
- ✅ Multiple summaries per session (v4.2.0+)
|
||||
- ✅ Summaries are checkpoints, not endings
|
||||
- ✅ Uses Claude Agent SDK for AI compression
|
||||
|
||||
**Summary structure:**
|
||||
```xml
|
||||
<summary>
|
||||
<request>User's original request</request>
|
||||
<investigated>What was examined</investigated>
|
||||
<learned>Key discoveries</learned>
|
||||
<completed>Work finished</completed>
|
||||
<next_steps>Remaining tasks</next_steps>
|
||||
<files_read>
|
||||
<file>path/to/file1.ts</file>
|
||||
<file>path/to/file2.ts</file>
|
||||
</files_read>
|
||||
<files_modified>
|
||||
<file>path/to/file3.ts</file>
|
||||
</files_modified>
|
||||
<notes>Additional context</notes>
|
||||
</summary>
|
||||
```
|
||||
|
||||
**Source:** `src/hooks/summary-hook.ts` → `plugin/scripts/summary-hook.js`
|
||||
|
||||
---
|
||||
|
||||
### Hook 5: SessionEnd (Cleanup Hook)
|
||||
|
||||
**Purpose:** Mark sessions as completed when they end
|
||||
|
||||
**When:** Claude Code session ends (not on `/clear`)
|
||||
|
||||
**What it does:**
|
||||
1. Marks session as completed in database
|
||||
2. Allows worker to finish processing
|
||||
3. Performs graceful cleanup
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionEnd": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ Graceful completion (v4.1.0+)
|
||||
- ✅ No longer sends DELETE to workers
|
||||
- ✅ Skips cleanup on `/clear` commands
|
||||
- ✅ Preserves ongoing sessions
|
||||
|
||||
**Why graceful cleanup?**
|
||||
|
||||
**Old approach (v3):**
|
||||
```typescript
|
||||
// ❌ Aggressive cleanup
|
||||
SessionEnd → DELETE /worker/session → Worker stops immediately
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Interrupted summary generation
|
||||
- Lost pending observations
|
||||
- Race conditions
|
||||
|
||||
**New approach (v4.1.0+):**
|
||||
```typescript
|
||||
// ✅ Graceful completion
|
||||
SessionEnd → UPDATE sessions SET completed_at = NOW()
|
||||
Worker sees completion → Finishes processing → Exits naturally
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Worker finishes important operations
|
||||
- Summaries complete successfully
|
||||
- Clean state transitions
|
||||
|
||||
**Source:** `src/hooks/cleanup-hook.ts` → `plugin/scripts/cleanup-hook.js`
|
||||
|
||||
---
|
||||
|
||||
## Hook Execution Flow
|
||||
|
||||
### Session Lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Claude
|
||||
participant Hooks
|
||||
participant Worker
|
||||
participant DB
|
||||
|
||||
User->>Claude: Start Claude Code
|
||||
Claude->>Hooks: SessionStart hook
|
||||
Hooks->>DB: Query recent context
|
||||
DB-->>Hooks: Session summaries + observations
|
||||
Hooks-->>Claude: Inject context
|
||||
Note over Claude: Context available for session
|
||||
|
||||
User->>Claude: Submit prompt
|
||||
Claude->>Hooks: UserPromptSubmit hook
|
||||
Hooks->>DB: Create session record
|
||||
Hooks->>Worker: Start worker (if not running)
|
||||
Worker-->>DB: Ready to process
|
||||
|
||||
Claude->>Claude: Execute tools
|
||||
Claude->>Hooks: PostToolUse (multiple times)
|
||||
Hooks->>DB: Queue observations
|
||||
Note over Worker: Polls queue, processes observations
|
||||
|
||||
Worker->>Worker: AI compression
|
||||
Worker->>DB: Store compressed observations
|
||||
Worker->>Hooks: Trigger summary hook
|
||||
Hooks->>DB: Store session summary
|
||||
|
||||
User->>Claude: Finish
|
||||
Claude->>Hooks: SessionEnd hook
|
||||
Hooks->>DB: Mark session complete
|
||||
Worker->>DB: Check completion
|
||||
Worker->>Worker: Finish processing
|
||||
Worker->>Worker: Exit gracefully
|
||||
```
|
||||
|
||||
### Hook Timing
|
||||
|
||||
| Event | Timing | Blocking | Timeout | Output Handling |
|
||||
|-------|--------|----------|---------|-----------------|
|
||||
| **SessionStart** | Before session | No | 120s | stdout → context |
|
||||
| **UserPromptSubmit** | Before processing | No | 60s | stdout → context |
|
||||
| **PostToolUse** | After tool | No | 60s | Transcript only |
|
||||
| **Summary** | Worker triggered | No | 300s | Database |
|
||||
| **SessionEnd** | On exit | No | 60s | Log only |
|
||||
|
||||
---
|
||||
|
||||
## The Worker Service Architecture
|
||||
|
||||
### Why a Background Worker?
|
||||
|
||||
**Problem:** Hooks must be fast (< 1 second)
|
||||
|
||||
**Reality:** AI compression takes 5-30 seconds per observation
|
||||
|
||||
**Solution:** Hooks enqueue observations, worker processes async
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ HOOK (Fast) │
|
||||
│ 1. Read stdin (< 1ms) │
|
||||
│ 2. Insert into queue (< 10ms) │
|
||||
│ 3. Return success (< 20ms total) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ (queue)
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ WORKER (Slow) │
|
||||
│ 1. Poll queue every 1s │
|
||||
│ 2. Process observation via Claude SDK (5-30s) │
|
||||
│ 3. Parse and store results │
|
||||
│ 4. Mark observation processed │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### PM2 Process Management
|
||||
|
||||
**Technology:** PM2 (process manager for Node.js)
|
||||
|
||||
**Why PM2:**
|
||||
- Auto-restart on failure
|
||||
- Log management
|
||||
- Process monitoring
|
||||
- Cross-platform (works on macOS, Linux, Windows)
|
||||
- No systemd/launchd needed
|
||||
|
||||
**Configuration:**
|
||||
```javascript
|
||||
// ecosystem.config.cjs
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '500M',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
CLAUDE_MEM_WORKER_PORT: 37777
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
**Worker lifecycle:**
|
||||
```bash
|
||||
# Started by new-hook (if not running)
|
||||
pm2 start ecosystem.config.cjs
|
||||
|
||||
# Status check
|
||||
pm2 status claude-mem-worker
|
||||
|
||||
# View logs
|
||||
pm2 logs claude-mem-worker
|
||||
|
||||
# Restart
|
||||
pm2 restart claude-mem-worker
|
||||
```
|
||||
|
||||
### Worker HTTP API
|
||||
|
||||
**Technology:** Express.js REST API on port 37777
|
||||
|
||||
**Endpoints:**
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/health` | GET | Health check |
|
||||
| `/sessions` | POST | Create session |
|
||||
| `/sessions/:id` | GET | Get session status |
|
||||
| `/sessions/:id` | PATCH | Update session |
|
||||
| `/observations` | POST | Enqueue observation |
|
||||
| `/observations/:id` | GET | Get observation |
|
||||
|
||||
**Why HTTP API?**
|
||||
- Language-agnostic (hooks can be any language)
|
||||
- Easy debugging (curl commands)
|
||||
- Standard error handling
|
||||
- Proper async handling
|
||||
|
||||
---
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### Pattern 1: Fire-and-Forget Hooks
|
||||
|
||||
**Principle:** Hooks should return immediately, not wait for completion
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Hook waits for processing
|
||||
export async function saveHook(stdin: HookInput) {
|
||||
const observation = parseInput(stdin);
|
||||
await processObservation(observation); // BLOCKS!
|
||||
return success();
|
||||
}
|
||||
|
||||
// ✅ Good: Hook enqueues and returns
|
||||
export async function saveHook(stdin: HookInput) {
|
||||
const observation = parseInput(stdin);
|
||||
await enqueueObservation(observation); // Fast
|
||||
return success(); // Immediate
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Queue-Based Processing
|
||||
|
||||
**Principle:** Decouple capture from processing
|
||||
|
||||
```
|
||||
Hook (capture) → Queue (buffer) → Worker (process)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Parallel hook execution safe
|
||||
- Worker failure doesn't affect hooks
|
||||
- Retry logic centralized
|
||||
- Backpressure handling
|
||||
|
||||
### Pattern 3: Graceful Degradation
|
||||
|
||||
**Principle:** Memory system failure shouldn't break Claude Code
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await captureObservation();
|
||||
} catch (error) {
|
||||
// Log error, but don't throw
|
||||
console.error('Memory capture failed:', error);
|
||||
return { continue: true, suppressOutput: true };
|
||||
}
|
||||
```
|
||||
|
||||
**Failure modes:**
|
||||
- Database locked → Skip observation, log error
|
||||
- Worker crashed → Auto-restart via PM2
|
||||
- Network issue → Retry with exponential backoff
|
||||
- Disk full → Warn user, disable memory
|
||||
|
||||
### Pattern 4: Progressive Enhancement
|
||||
|
||||
**Principle:** Core functionality works without memory, memory enhances it
|
||||
|
||||
```
|
||||
Without memory: Claude Code works normally
|
||||
With memory: Claude Code + context from past sessions
|
||||
Memory broken: Falls back to working normally
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hook Debugging
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable detailed hook execution logs:
|
||||
|
||||
```bash
|
||||
claude --debug
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
[DEBUG] Executing hooks for PostToolUse:Write
|
||||
[DEBUG] Getting matching hook commands for PostToolUse with query: Write
|
||||
[DEBUG] Found 1 hook matchers in settings
|
||||
[DEBUG] Matched 1 hooks for query "Write"
|
||||
[DEBUG] Found 1 hook commands to execute
|
||||
[DEBUG] Executing hook command: ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js with timeout 60000ms
|
||||
[DEBUG] Hook command completed with status 0: {"continue":true,"suppressOutput":true}
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Hook not executing">
|
||||
**Symptoms:** Hook command never runs
|
||||
|
||||
**Debugging:**
|
||||
1. Check `/hooks` menu - is hook registered?
|
||||
2. Verify matcher pattern (case-sensitive!)
|
||||
3. Test command manually: `echo '{}' | node save-hook.js`
|
||||
4. Check file permissions (executable?)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Hook times out">
|
||||
**Symptoms:** Hook execution exceeds timeout
|
||||
|
||||
**Debugging:**
|
||||
1. Check timeout setting (default 60s)
|
||||
2. Identify slow operation (database? network?)
|
||||
3. Move slow operation to worker
|
||||
4. Increase timeout if necessary
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Context not injecting">
|
||||
**Symptoms:** SessionStart hook runs but context missing
|
||||
|
||||
**Debugging:**
|
||||
1. Check stdout (must be valid JSON or plain text)
|
||||
2. Verify no stderr output (pollutes JSON)
|
||||
3. Check exit code (must be 0)
|
||||
4. Look for npm install output (v4.3.1 fix)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Observations not captured">
|
||||
**Symptoms:** PostToolUse hook runs but observations missing
|
||||
|
||||
**Debugging:**
|
||||
1. Check database: `sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM observation_queue"`
|
||||
2. Verify session exists: `SELECT * FROM sdk_sessions`
|
||||
3. Check worker status: `pm2 status`
|
||||
4. View worker logs: `pm2 logs claude-mem-worker`
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Testing Hooks Manually
|
||||
|
||||
```bash
|
||||
# Test context hook
|
||||
echo '{
|
||||
"session_id": "test123",
|
||||
"cwd": "/Users/alex/projects/my-app",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}' | node plugin/scripts/context-hook.js
|
||||
|
||||
# Test save hook
|
||||
echo '{
|
||||
"session_id": "test123",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {"file_path": "test.ts"},
|
||||
"tool_output": {"success": true}
|
||||
}' | node plugin/scripts/save-hook.js
|
||||
|
||||
# Test with actual Claude Code
|
||||
claude --debug
|
||||
/hooks # View registered hooks
|
||||
# Submit prompt and watch debug output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Hook Execution Time
|
||||
|
||||
**Target:** < 100ms per hook
|
||||
|
||||
**Actual measurements:**
|
||||
|
||||
| Hook | Average | p95 | p99 |
|
||||
|------|---------|-----|-----|
|
||||
| SessionStart | 45ms | 120ms | 250ms |
|
||||
| UserPromptSubmit | 12ms | 25ms | 50ms |
|
||||
| PostToolUse | 8ms | 15ms | 30ms |
|
||||
| SessionEnd | 5ms | 10ms | 20ms |
|
||||
|
||||
**Why SessionStart is slower:**
|
||||
- npm install check (idempotent but runs every time)
|
||||
- Database query for 10 sessions + 50 observations
|
||||
- Formatting progressive disclosure index
|
||||
|
||||
**Optimization (v4.3.1):**
|
||||
- Use `--loglevel=silent` for npm install
|
||||
- Cache package.json hash to skip unnecessary installs
|
||||
- Use prepared statements for database queries
|
||||
|
||||
### Database Performance
|
||||
|
||||
**Schema optimizations:**
|
||||
- Indexes on `project`, `created_at_epoch`, `claude_session_id`
|
||||
- FTS5 virtual tables for full-text search
|
||||
- WAL mode for concurrent reads/writes
|
||||
|
||||
**Query patterns:**
|
||||
```sql
|
||||
-- Fast: Uses index on (project, created_at_epoch)
|
||||
SELECT * FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
|
||||
-- Fast: Uses index on claude_session_id
|
||||
SELECT * FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
|
||||
-- Fast: FTS5 full-text search
|
||||
SELECT * FROM observations_fts
|
||||
WHERE observations_fts MATCH ?
|
||||
ORDER BY rank
|
||||
LIMIT 20
|
||||
```
|
||||
|
||||
### Worker Throughput
|
||||
|
||||
**Bottleneck:** Claude API latency (5-30s per observation)
|
||||
|
||||
**Mitigation:**
|
||||
- Process observations sequentially (simpler, more predictable)
|
||||
- Skip low-value observations (TodoWrite, ListMcpResourcesTool)
|
||||
- Batch summaries (generate every N observations, not every observation)
|
||||
|
||||
**Future optimization:**
|
||||
- Parallel processing (multiple workers)
|
||||
- Smart batching (combine related observations)
|
||||
- Lazy summarization (summarize only when needed)
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Hook Command Safety
|
||||
|
||||
**Risk:** Hooks execute arbitrary commands with user permissions
|
||||
|
||||
**Mitigations:**
|
||||
1. **Frozen at startup:** Hook configuration captured at start, changes require review
|
||||
2. **User review required:** `/hooks` menu shows changes, requires approval
|
||||
3. **Plugin isolation:** `${CLAUDE_PLUGIN_ROOT}` prevents path traversal
|
||||
4. **Input validation:** Hooks validate stdin schema before processing
|
||||
|
||||
### Data Privacy
|
||||
|
||||
**What gets stored:**
|
||||
- User prompts (raw text) - v4.2.0+
|
||||
- Tool inputs and outputs
|
||||
- File paths read/modified
|
||||
- Session summaries
|
||||
|
||||
**Privacy guarantees:**
|
||||
- All data stored locally in `~/.claude-mem/claude-mem.db`
|
||||
- No cloud uploads (API calls only for AI compression)
|
||||
- SQLite file permissions: user-only read/write
|
||||
- No analytics or telemetry
|
||||
|
||||
### API Key Protection
|
||||
|
||||
**Configuration:**
|
||||
- Anthropic API key in `~/.anthropic/api_key` or `ANTHROPIC_API_KEY` env var
|
||||
- Worker inherits environment from Claude Code
|
||||
- Never logged or stored in database
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Hooks are interfaces**: They define clean boundaries between systems
|
||||
2. **Non-blocking is critical**: Hooks must return fast, workers do the heavy lifting
|
||||
3. **Graceful degradation**: Memory system can fail without breaking Claude Code
|
||||
4. **Queue-based decoupling**: Capture and processing happen independently
|
||||
5. **Progressive disclosure**: Context injection uses index-first approach
|
||||
6. **Lifecycle alignment**: Each hook has a clear, single purpose
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Claude Code Hooks Reference](https://docs.claude.com/claude-code/hooks) - Official documentation
|
||||
- [Progressive Disclosure](/docs/progressive-disclosure) - Context priming philosophy
|
||||
- [Architecture Evolution](/docs/architecture-evolution) - v3 to v4 journey
|
||||
- [Worker Service Design](/docs/worker-service) - Background processing details
|
||||
|
||||
---
|
||||
|
||||
*The hook-driven architecture enables Claude-Mem to be both powerful and invisible. Users never notice the memory system working - it just makes Claude smarter over time.*
|
||||
+13
-8
@@ -67,16 +67,21 @@ See [Architecture Overview](architecture/overview) for details.
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## What's New in v4.2.3
|
||||
## What's New in v4.3.1
|
||||
|
||||
**Security:**
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
**Critical Fix:**
|
||||
- Fixed SessionStart hook context injection (v4.3.1)
|
||||
- Context wasn't being injected due to npm output pollution
|
||||
- Changed npm loglevel to `--loglevel=silent` for clean JSON output
|
||||
|
||||
**Fixes:**
|
||||
- Fixed ESM/CJS compatibility for getDirname function
|
||||
- Fixed Windows PowerShell compatibility in SessionStart hook
|
||||
- Cross-platform dependency installation now works on Windows, macOS, and Linux
|
||||
**Code Quality:**
|
||||
- Consolidated hooks architecture (removed bin/hooks wrapper layer)
|
||||
- Fixed double shebang issues in hook executables
|
||||
|
||||
**Recent Updates (v4.3.0):**
|
||||
- Progressive disclosure context with observation timelines
|
||||
- Enhanced session summaries with token cost visibility
|
||||
- Cross-platform path detection improvements
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
# Progressive Disclosure: Claude-Mem's Context Priming Philosophy
|
||||
|
||||
## Core Principle
|
||||
**Show what exists and its retrieval cost first. Let the agent decide what to fetch based on relevance and need.**
|
||||
|
||||
---
|
||||
|
||||
## What is Progressive Disclosure?
|
||||
|
||||
Progressive disclosure is an information architecture pattern where you reveal complexity gradually rather than all at once. In the context of AI agents, it means:
|
||||
|
||||
1. **Layer 1 (Index)**: Show lightweight metadata (titles, dates, types, token counts)
|
||||
2. **Layer 2 (Details)**: Fetch full content only when needed
|
||||
3. **Layer 3 (Deep Dive)**: Read original source files if required
|
||||
|
||||
This mirrors how humans work: We scan headlines before reading articles, review table of contents before diving into chapters, and check file names before opening files.
|
||||
|
||||
---
|
||||
|
||||
## The Problem: Context Pollution
|
||||
|
||||
Traditional RAG (Retrieval-Augmented Generation) systems fetch everything upfront:
|
||||
|
||||
```
|
||||
❌ Traditional Approach:
|
||||
┌─────────────────────────────────────┐
|
||||
│ Session Start │
|
||||
│ │
|
||||
│ [15,000 tokens of past sessions] │
|
||||
│ [8,000 tokens of observations] │
|
||||
│ [12,000 tokens of file summaries] │
|
||||
│ │
|
||||
│ Total: 35,000 tokens │
|
||||
│ Relevant: ~2,000 tokens (6%) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Wastes 94% of attention budget on irrelevant context
|
||||
- User prompt gets buried under mountain of history
|
||||
- Agent must process everything before understanding task
|
||||
- No way to know what's actually useful until after reading
|
||||
|
||||
---
|
||||
|
||||
## Claude-Mem's Solution: Progressive Disclosure
|
||||
|
||||
```
|
||||
✅ Progressive Disclosure Approach:
|
||||
┌─────────────────────────────────────┐
|
||||
│ Session Start │
|
||||
│ │
|
||||
│ Index of 50 observations: ~800 tokens│
|
||||
│ ↓ │
|
||||
│ Agent sees: "🔴 Hook timeout issue" │
|
||||
│ Agent decides: "Relevant!" │
|
||||
│ ↓ │
|
||||
│ Fetch observation #2543: ~120 tokens│
|
||||
│ │
|
||||
│ Total: 920 tokens │
|
||||
│ Relevant: 920 tokens (100%) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Agent controls its own context consumption
|
||||
- Directly relevant to current task
|
||||
- Can fetch more if needed
|
||||
- Can skip everything if not relevant
|
||||
- Clear cost/benefit for each retrieval decision
|
||||
|
||||
---
|
||||
|
||||
## How It Works in Claude-Mem
|
||||
|
||||
### The Index Format
|
||||
|
||||
Every SessionStart hook provides a compact index:
|
||||
|
||||
```markdown
|
||||
### Oct 26, 2025
|
||||
|
||||
**General**
|
||||
| ID | Time | T | Title | Tokens |
|
||||
|----|------|---|-------|--------|
|
||||
| #2586 | 12:58 AM | 🔵 | Context hook file exists but is empty | ~51 |
|
||||
| #2587 | ″ | 🔵 | Context hook script file is empty | ~46 |
|
||||
| #2589 | ″ | 🟡 | Investigated hook debug output docs | ~105 |
|
||||
|
||||
**src/hooks/context-hook.ts**
|
||||
| ID | Time | T | Title | Tokens |
|
||||
|----|------|---|-------|--------|
|
||||
| #2591 | 1:15 AM | ⚖️ | Stderr messaging abandoned | ~155 |
|
||||
| #2592 | 1:16 AM | ⚖️ | Web UI strategy redesigned | ~193 |
|
||||
```
|
||||
|
||||
**What the agent sees:**
|
||||
- **What exists**: Observation titles give semantic meaning
|
||||
- **When it happened**: Timestamps for temporal context
|
||||
- **What type**: Icons indicate observation category
|
||||
- **Retrieval cost**: Token counts for informed decisions
|
||||
- **Where to get it**: MCP search tools referenced at bottom
|
||||
|
||||
### The Legend System
|
||||
|
||||
```
|
||||
🎯 session-request - User's original goal
|
||||
🔴 gotcha - Critical edge case or pitfall
|
||||
🟡 problem-solution - Bug fix or workaround
|
||||
🔵 how-it-works - Technical explanation
|
||||
🟢 what-changed - Code/architecture change
|
||||
🟣 discovery - Learning or insight
|
||||
🟠 why-it-exists - Design rationale
|
||||
🟤 decision - Architecture decision
|
||||
⚖️ trade-off - Deliberate compromise
|
||||
```
|
||||
|
||||
**Purpose:**
|
||||
- Visual scanning (humans and AI both benefit)
|
||||
- Semantic categorization
|
||||
- Priority signaling (🔴 gotchas are more critical)
|
||||
- Pattern recognition across sessions
|
||||
|
||||
### Progressive Disclosure Instructions
|
||||
|
||||
The index includes usage guidance:
|
||||
|
||||
```markdown
|
||||
💡 **Progressive Disclosure:** This index shows WHAT exists and retrieval COST.
|
||||
- Use MCP search tools to fetch full observation details on-demand
|
||||
- Prefer searching observations over re-reading code for past decisions
|
||||
- Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- Teaches the agent the pattern
|
||||
- Suggests when to fetch (critical types)
|
||||
- Recommends search over code re-reading (efficiency)
|
||||
- Makes the system self-documenting
|
||||
|
||||
---
|
||||
|
||||
## The Philosophy: Context as Currency
|
||||
|
||||
### Mental Model: Token Budget as Money
|
||||
|
||||
Think of context window as a bank account:
|
||||
|
||||
| Approach | Metaphor | Outcome |
|
||||
|----------|----------|---------|
|
||||
| **Dump everything** | Spending your entire paycheck on groceries you might need someday | Waste, clutter, can't afford what you actually need |
|
||||
| **Fetch nothing** | Refusing to spend any money | Starvation, can't accomplish tasks |
|
||||
| **Progressive disclosure** | Check your pantry, make a shopping list, buy only what you need | Efficiency, room for unexpected needs |
|
||||
|
||||
### The Attention Budget
|
||||
|
||||
LLMs have finite attention:
|
||||
- Every token attends to every other token (n² relationships)
|
||||
- 100,000 token window ≠ 100,000 tokens of useful attention
|
||||
- Context "rot" happens as window fills
|
||||
- Later tokens get less attention than earlier ones
|
||||
|
||||
**Claude-Mem's approach:**
|
||||
- Start with ~1,000 tokens of index
|
||||
- Agent has 99,000 tokens free for task
|
||||
- Agent fetches ~200 tokens when needed
|
||||
- Final budget: ~98,000 tokens for actual work
|
||||
|
||||
### Design for Autonomy
|
||||
|
||||
> "As models improve, let them act intelligently"
|
||||
|
||||
Progressive disclosure treats the agent as an **intelligent information forager**, not a passive recipient of pre-selected context.
|
||||
|
||||
**Traditional RAG:**
|
||||
```
|
||||
System → [Decides relevance] → Agent
|
||||
↑
|
||||
Hope this helps!
|
||||
```
|
||||
|
||||
**Progressive Disclosure:**
|
||||
```
|
||||
System → [Shows index] → Agent → [Decides relevance] → [Fetches details]
|
||||
↑
|
||||
You know best!
|
||||
```
|
||||
|
||||
The agent knows:
|
||||
- The current task context
|
||||
- What information would help
|
||||
- How much budget to spend
|
||||
- When to stop searching
|
||||
|
||||
We don't.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Principles
|
||||
|
||||
### 1. Make Costs Visible
|
||||
|
||||
Every item in the index shows token count:
|
||||
|
||||
```
|
||||
| #2591 | 1:15 AM | ⚖️ | Stderr messaging abandoned | ~155 |
|
||||
^^^^
|
||||
Retrieval cost
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Agent can make informed ROI decisions
|
||||
- Small observations (~50 tokens) are "cheap" to fetch
|
||||
- Large observations (~500 tokens) require stronger justification
|
||||
- Matches how humans think about effort
|
||||
|
||||
### 2. Use Semantic Compression
|
||||
|
||||
Titles compress full observations into ~10 words:
|
||||
|
||||
**Bad title:**
|
||||
```
|
||||
Observation about a thing
|
||||
```
|
||||
|
||||
**Good title:**
|
||||
```
|
||||
🔴 Hook timeout issue: 60s default too short for npm install
|
||||
```
|
||||
|
||||
**What makes a good title:**
|
||||
- Specific: Identifies exact issue
|
||||
- Actionable: Clear what to do
|
||||
- Self-contained: Doesn't require reading observation
|
||||
- Searchable: Contains key terms (hook, timeout, npm)
|
||||
- Categorized: Icon indicates type
|
||||
|
||||
### 3. Group by Context
|
||||
|
||||
Observations are grouped by:
|
||||
- **Date**: Temporal context
|
||||
- **File path**: Spatial context (work on specific files)
|
||||
- **Project**: Logical context
|
||||
|
||||
```markdown
|
||||
**src/hooks/context-hook.ts**
|
||||
| ID | Time | T | Title | Tokens |
|
||||
|----|------|---|-------|--------|
|
||||
| #2591 | 1:15 AM | ⚖️ | Stderr messaging abandoned | ~155 |
|
||||
| #2594 | 1:17 AM | 🟠 | Removed stderr section from docs | ~93 |
|
||||
```
|
||||
|
||||
**Benefit:** If agent is working on `src/hooks/context-hook.ts`, related observations are already grouped together.
|
||||
|
||||
### 4. Provide Retrieval Tools
|
||||
|
||||
The index is useless without retrieval mechanisms:
|
||||
|
||||
```markdown
|
||||
*Use claude-mem MCP search to access records with the given ID*
|
||||
```
|
||||
|
||||
**Available tools:**
|
||||
- `search_observations` - Full-text search
|
||||
- `find_by_concept` - Concept-based retrieval
|
||||
- `find_by_file` - File-based retrieval
|
||||
- `find_by_type` - Type-based retrieval
|
||||
- `get_recent_context` - Recent session summaries
|
||||
|
||||
Each tool supports `format: "index"` (default) and `format: "full"`.
|
||||
|
||||
---
|
||||
|
||||
## Real-World Example
|
||||
|
||||
### Scenario: Agent asked to fix a bug in hooks
|
||||
|
||||
**Without progressive disclosure:**
|
||||
```
|
||||
SessionStart injects 25,000 tokens of past context
|
||||
Agent reads everything
|
||||
Agent finds 1 relevant observation (buried in middle)
|
||||
Total tokens consumed: 25,000
|
||||
Relevant tokens: ~200
|
||||
Efficiency: 0.8%
|
||||
```
|
||||
|
||||
**With progressive disclosure:**
|
||||
```
|
||||
SessionStart shows index: ~800 tokens
|
||||
Agent sees title: "🔴 Hook timeout issue: 60s too short"
|
||||
Agent thinks: "This looks relevant to my bug!"
|
||||
Agent fetches observation #2543: ~155 tokens
|
||||
Total tokens consumed: 955
|
||||
Relevant tokens: 955
|
||||
Efficiency: 100%
|
||||
```
|
||||
|
||||
### The Index Entry
|
||||
|
||||
```markdown
|
||||
| #2543 | 2:14 PM | 🔴 | Hook timeout: 60s too short for npm install | ~155 |
|
||||
```
|
||||
|
||||
**What the agent learns WITHOUT fetching:**
|
||||
- There's a known gotcha (🔴) about hook timeouts
|
||||
- It's related to npm install taking too long
|
||||
- Full details are ~155 tokens (cheap)
|
||||
- Happened at 2:14 PM (recent)
|
||||
|
||||
**Decision tree:**
|
||||
```
|
||||
Is my task related to hooks? → YES
|
||||
Is my task related to timeouts? → YES
|
||||
Is my task related to npm? → YES
|
||||
155 tokens is cheap → FETCH IT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Two-Tier Search Strategy
|
||||
|
||||
Claude-Mem implements progressive disclosure in search results too:
|
||||
|
||||
### Tier 1: Index Format (Default)
|
||||
|
||||
```typescript
|
||||
search_observations({
|
||||
query: "hook timeout",
|
||||
format: "index" // Default
|
||||
})
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```
|
||||
Found 3 observations matching "hook timeout":
|
||||
|
||||
| ID | Date | Type | Title | Tokens |
|
||||
|----|------|------|-------|--------|
|
||||
| #2543 | Oct 26 | gotcha | Hook timeout: 60s too short | ~155 |
|
||||
| #2891 | Oct 25 | how-it-works | Hook timeout configuration | ~203 |
|
||||
| #2102 | Oct 20 | problem-solution | Fixed timeout in CI | ~89 |
|
||||
```
|
||||
|
||||
**Cost:** ~100 tokens for 3 results
|
||||
**Value:** Agent can scan and decide which to fetch
|
||||
|
||||
### Tier 2: Full Format (On-Demand)
|
||||
|
||||
```typescript
|
||||
search_observations({
|
||||
query: "hook timeout",
|
||||
format: "full",
|
||||
limit: 1 // Fetch just the most relevant
|
||||
})
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```
|
||||
#2543 🔴 Hook timeout: 60s too short for npm install
|
||||
─────────────────────────────────────────────────
|
||||
Date: Oct 26, 2025 2:14 PM
|
||||
Type: gotcha
|
||||
Project: claude-mem
|
||||
|
||||
Narrative:
|
||||
Discovered that the default 60-second hook timeout is insufficient
|
||||
for npm install operations, especially with large dependency trees
|
||||
or slow network conditions. This causes SessionStart hook to fail
|
||||
silently, preventing context injection.
|
||||
|
||||
Facts:
|
||||
- Default timeout: 60 seconds
|
||||
- npm install with cold cache: ~90 seconds
|
||||
- Configured timeout: 120 seconds in plugin/hooks/hooks.json:25
|
||||
|
||||
Files Modified:
|
||||
- plugin/hooks/hooks.json
|
||||
|
||||
Concepts: hooks, timeout, npm, configuration
|
||||
```
|
||||
|
||||
**Cost:** ~155 tokens for full details
|
||||
**Value:** Complete understanding of the issue
|
||||
|
||||
---
|
||||
|
||||
## Cognitive Load Theory
|
||||
|
||||
Progressive disclosure is grounded in **Cognitive Load Theory**:
|
||||
|
||||
### Intrinsic Load
|
||||
The inherent difficulty of the task itself.
|
||||
|
||||
**Example:** "Fix authentication bug"
|
||||
- Must understand auth system
|
||||
- Must understand the bug
|
||||
- Must write the fix
|
||||
|
||||
This load is unavoidable.
|
||||
|
||||
### Extraneous Load
|
||||
The cognitive burden of poorly presented information.
|
||||
|
||||
**Traditional RAG adds extraneous load:**
|
||||
- Scanning irrelevant observations
|
||||
- Filtering out noise
|
||||
- Remembering what to ignore
|
||||
- Re-contextualizing after each section
|
||||
|
||||
**Progressive disclosure minimizes extraneous load:**
|
||||
- Scan titles (low effort)
|
||||
- Fetch only relevant (targeted effort)
|
||||
- Full attention on current task
|
||||
|
||||
### Germane Load
|
||||
The effort of building mental models and schemas.
|
||||
|
||||
**Progressive disclosure supports germane load:**
|
||||
- Consistent structure (legend, grouping)
|
||||
- Clear categorization (types, icons)
|
||||
- Semantic compression (good titles)
|
||||
- Explicit costs (token counts)
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Verbose Titles
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
| #2543 | 2:14 PM | 🔴 | Investigation into the issue where hooks time out | ~155 |
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```
|
||||
| #2543 | 2:14 PM | 🔴 | Hook timeout: 60s too short for npm install | ~155 |
|
||||
```
|
||||
|
||||
### ❌ Hiding Costs
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
| #2543 | 2:14 PM | 🔴 | Hook timeout issue |
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```
|
||||
| #2543 | 2:14 PM | 🔴 | Hook timeout issue | ~155 |
|
||||
```
|
||||
|
||||
### ❌ No Retrieval Path
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
Here are 10 observations. [No instructions on how to get full details]
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```
|
||||
Here are 10 observations.
|
||||
*Use MCP search tools to fetch full observation details on-demand*
|
||||
```
|
||||
|
||||
### ❌ Defaulting to Full Format
|
||||
|
||||
**Bad:**
|
||||
```typescript
|
||||
search_observations({
|
||||
query: "hooks",
|
||||
format: "full" // Fetches everything
|
||||
})
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```typescript
|
||||
search_observations({
|
||||
query: "hooks",
|
||||
format: "index", // Scan first
|
||||
limit: 20
|
||||
})
|
||||
|
||||
// Then, if needed:
|
||||
search_observations({
|
||||
query: "hooks",
|
||||
format: "full",
|
||||
limit: 1 // Just the most relevant
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Why Token Counts?
|
||||
|
||||
**Decision:** Show approximate token counts (~155, ~203) rather than exact counts.
|
||||
|
||||
**Rationale:**
|
||||
- Communicates scale (50 vs 500) without false precision
|
||||
- Maps to human intuition (small/medium/large)
|
||||
- Allows agent to budget attention
|
||||
- Encourages cost-conscious retrieval
|
||||
|
||||
### Why Icons Instead of Text Labels?
|
||||
|
||||
**Decision:** Use emoji icons (🔴, 🟡, 🔵) rather than text (GOTCHA, PROBLEM, HOWTO).
|
||||
|
||||
**Rationale:**
|
||||
- Visual scanning (pattern recognition)
|
||||
- Token efficient (1 char vs 10 chars)
|
||||
- Language-agnostic
|
||||
- Aesthetically distinct
|
||||
- Works for both humans and AI
|
||||
|
||||
### Why Index-First, Not Smart Pre-Fetch?
|
||||
|
||||
**Decision:** Always show index first, even if we "know" what's relevant.
|
||||
|
||||
**Rationale:**
|
||||
- We can't know what's relevant better than the agent
|
||||
- Pre-fetching assumes we understand the task
|
||||
- Agent knows current context, we don't
|
||||
- Respects agent autonomy
|
||||
- Fails gracefully (can always fetch more)
|
||||
|
||||
### Why Group by File Path?
|
||||
|
||||
**Decision:** Group observations by file path in addition to date.
|
||||
|
||||
**Rationale:**
|
||||
- Spatial locality: Work on file X likely needs context about file X
|
||||
- Reduces scanning effort
|
||||
- Matches how developers think
|
||||
- Clear semantic boundaries
|
||||
|
||||
---
|
||||
|
||||
## Measuring Success
|
||||
|
||||
Progressive disclosure is working when:
|
||||
|
||||
### ✅ Low Waste Ratio
|
||||
```
|
||||
Relevant Tokens / Total Context Tokens > 80%
|
||||
```
|
||||
|
||||
Most of the context consumed is actually useful.
|
||||
|
||||
### ✅ Selective Fetching
|
||||
```
|
||||
Index Shown: 50 observations
|
||||
Details Fetched: 2-3 observations
|
||||
```
|
||||
|
||||
Agent is being selective, not fetching everything.
|
||||
|
||||
### ✅ Fast Task Completion
|
||||
```
|
||||
Session with index: 30 seconds to find relevant context
|
||||
Session without: 90 seconds scanning all context
|
||||
```
|
||||
|
||||
Time-to-relevant-information is faster.
|
||||
|
||||
### ✅ Appropriate Depth
|
||||
```
|
||||
Simple task: Only index needed
|
||||
Medium task: 1-2 observations fetched
|
||||
Complex task: 5-10 observations + code reads
|
||||
```
|
||||
|
||||
Depth scales with task complexity.
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Adaptive Index Size
|
||||
|
||||
```typescript
|
||||
// Vary index size based on session type
|
||||
SessionStart({ source: "startup" }):
|
||||
→ Show last 10 sessions (small index)
|
||||
|
||||
SessionStart({ source: "resume" }):
|
||||
→ Show only current session (micro index)
|
||||
|
||||
SessionStart({ source: "compact" }):
|
||||
→ Show last 20 sessions (larger index)
|
||||
```
|
||||
|
||||
### Relevance Scoring
|
||||
|
||||
```typescript
|
||||
// Use embeddings to pre-sort index by relevance
|
||||
search_observations({
|
||||
query: "authentication bug",
|
||||
format: "index",
|
||||
sort: "relevance" // Based on semantic similarity
|
||||
})
|
||||
```
|
||||
|
||||
### Cost Forecasting
|
||||
|
||||
```markdown
|
||||
💡 **Budget Estimate:**
|
||||
- Fetching all 🔴 gotchas: ~450 tokens
|
||||
- Fetching all file-related: ~1,200 tokens
|
||||
- Fetching everything: ~8,500 tokens
|
||||
```
|
||||
|
||||
### Progressive Detail Levels
|
||||
|
||||
```
|
||||
Layer 1: Index (titles only)
|
||||
Layer 2: Summaries (2-3 sentences)
|
||||
Layer 3: Full details (complete observation)
|
||||
Layer 4: Source files (referenced code)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Show, don't tell**: Index reveals what exists without forcing consumption
|
||||
2. **Cost-conscious**: Make retrieval costs visible for informed decisions
|
||||
3. **Agent autonomy**: Let the agent decide what's relevant
|
||||
4. **Semantic compression**: Good titles make or break the system
|
||||
5. **Consistent structure**: Patterns reduce cognitive load
|
||||
6. **Two-tier everything**: Index first, details on-demand
|
||||
7. **Context as currency**: Spend wisely on high-value information
|
||||
|
||||
---
|
||||
|
||||
## Remember
|
||||
|
||||
> "The best interface is one that disappears when not needed, and appears exactly when it is."
|
||||
|
||||
Progressive disclosure respects the agent's intelligence and autonomy. We provide the map; the agent chooses the path.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Context Engineering for AI Agents](/docs/context-engineering) - Foundational principles
|
||||
- [Claude-Mem Architecture](/docs/architecture) - How it all fits together
|
||||
- Cognitive Load Theory (Sweller, 1988)
|
||||
- Information Foraging Theory (Pirolli & Card, 1999)
|
||||
- Progressive Disclosure (Nielsen Norman Group)
|
||||
|
||||
---
|
||||
|
||||
*This philosophy emerged from real-world usage of Claude-Mem across hundreds of coding sessions. The pattern works because it aligns with both human cognition and LLM attention mechanics.*
|
||||
@@ -1098,7 +1098,6 @@ Skip routine operations:
|
||||
|----------|---------|---------|----------------|
|
||||
| `CLAUDE_MEM_MODEL` | `claude-sonnet-4-5` | AI model for processing | Invalid = SDK fails |
|
||||
| `CLAUDE_MEM_WORKER_PORT` | `37777` | HTTP server port | Invalid = Worker won't start |
|
||||
| `CLAUDE_CODE_PATH` | `/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude` | Path to Claude Code | Invalid = SDK fails |
|
||||
|
||||
### Constants
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = {
|
||||
max_memory_restart: '500M',
|
||||
min_uptime: '10s',
|
||||
max_restarts: 10,
|
||||
restart_delay: 4000,
|
||||
restart_delay: 0,
|
||||
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
@@ -45,8 +45,8 @@ module.exports = {
|
||||
log_type: 'json',
|
||||
|
||||
// Process management
|
||||
kill_timeout: 5000,
|
||||
listen_timeout: 10000,
|
||||
kill_timeout: 1000,
|
||||
listen_timeout: 3000,
|
||||
shutdown_with_message: true,
|
||||
|
||||
// PM2 Plus (optional monitoring)
|
||||
|
||||
Generated
+8
-6
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.1",
|
||||
"version": "4.2.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.1",
|
||||
"version": "4.2.10",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.23",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -31,9 +31,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.23.tgz",
|
||||
"integrity": "sha512-DktXOjzS2hOuuj2Zpo7fEooONfMa5bm09pt1/Vt4vn30YugELoezn/ZQ/TG5uSQ7+Zl/ElxAvi4vGDorj1Tirg==",
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.27.tgz",
|
||||
"integrity": "sha512-HuMPW6spj2q8FODiP/WBCqUZAYGwDPoI1EpicP9KUXvuYk+2MZQYSaD7oiN6iNPupR2T5oJ2HY/D9OzjyCD2Mw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -2304,6 +2304,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -5242,6 +5243,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.9",
|
||||
"version": "4.3.4",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -41,7 +41,7 @@
|
||||
"worker:logs": "pm2 logs claude-mem-worker"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.23",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.9",
|
||||
"version": "4.3.4",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -6,8 +6,13 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=silent && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 300
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import P from"better-sqlite3";import{join as c,dirname as U,basename as V}from"path";import{homedir as f}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||c(f(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||c(f(),".claude"),ee=c(p,"archives"),se=c(p,"logs"),te=c(p,"trash"),re=c(p,"backups"),ne=c(p,"settings.json"),I=c(p,"claude-mem.db"),oe=c(u,"settings.json"),ie=c(u,"commands"),ae=c(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return c(F,"..","..")}var l=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(l||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=l[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import{stdin as N}from"process";import F from"better-sqlite3";import{join as p,dirname as x,basename as Y}from"path";import{homedir as h}from"os";import{existsSync as Q,mkdirSync as U}from"fs";import{fileURLToPath as w}from"url";function X(){return typeof __dirname<"u"?__dirname:x(w(import.meta.url))}var M=X(),c=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),Z=p(c,"archives"),ee=p(c,"logs"),se=p(c,"trash"),te=p(c,"backups"),re=p(c,"settings.json"),I=p(c,"claude-mem.db"),ne=p(u,"settings.json"),oe=p(u,"commands"),ie=p(u,"CLAUDE.md");function O(o){U(o,{recursive:!0})}function L(){return p(M,"..","..")}var l=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(l||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=l[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=l[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let b="";if(r){let{sessionId:B,sdkSessionId:j,correlationId:$,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new T;var m=class{db;constructor(){O(p),this.db=new P(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let b="";if(r){let{sessionId:H,sdkSessionId:B,correlationId:j,...f}=r;Object.keys(f).length>0&&(b=` {${Object.entries(f).map(([D,y])=>`${D}=${y}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new T;var m=class{db;constructor(){O(c),this.db=new F(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -306,5 +306,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import S from"path";import{existsSync as g}from"fs";import{spawn as G}from"child_process";var H=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),W=`http://127.0.0.1:${H}/health`;async function v(){try{return(await fetch(W,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await v())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!g(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!g(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!g(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await v())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function k(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await C()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var N="";D.on("data",o=>N+=o);D.on("end",async()=>{try{let o=N.trim()?JSON.parse(N):void 0;await k(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import S from"path";import{existsSync as g}from"fs";import{spawn as P}from"child_process";var G=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),W=`http://127.0.0.1:${G}/health`;async function v(){try{return(await fetch(W,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await v())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!g(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!g(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!g(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=P(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await v())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function k(o){console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await C()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port}),r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database"),r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(N.isTTY)k(void 0);else{let o="";N.on("data",e=>o+=e),N.on("end",async()=>{let e=o?JSON.parse(o):void 0;await k(e)})}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import C from"path";import q from"better-sqlite3";import{join as _,dirname as W,basename as z}from"path";import{homedir as U}from"os";import{existsSync as te,mkdirSync as j}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:W(H(import.meta.url))}var Y=B(),m=process.env.CLAUDE_MEM_DATA_DIR||_(U(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||_(U(),".claude"),ne=_(m,"archives"),ie=_(m,"logs"),oe=_(m,"trash"),ae=_(m,"backups"),de=_(m,"settings.json"),w=_(m,"claude-mem.db"),pe=_(O,"settings.json"),ce=_(O,"commands"),_e=_(O,"CLAUDE.md");function $(p){j(p,{recursive:!0})}function M(){return _(Y,"..","..")}var L=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(L||{}),A=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=L[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),n=L[e].padEnd(5),c=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let a="";i!=null&&(this.level===0&&typeof i=="object"?a=`
|
||||
`+JSON.stringify(i,null,2):a=" "+this.formatData(i));let l="";if(r){let{sessionId:G,sdkSessionId:k,correlationId:R,...T}=r;Object.keys(T).length>0&&(l=` {${Object.entries(T).map(([g,b])=>`${g}=${b}`).join(", ")}}`)}let f=`[${d}] [${n}] [${c}] ${E}${t}${l}${a}`;e===3?console.error(f):console.log(f)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},X=new A;var N=class{db;constructor(){$(m),this.db=new q(w),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import W from"path";import{stdin as P}from"process";import ce from"better-sqlite3";import{join as T,dirname as ne,basename as be}from"path";import{homedir as j}from"os";import{existsSync as Oe,mkdirSync as ie}from"fs";import{fileURLToPath as oe}from"url";function ae(){return typeof __dirname<"u"?__dirname:ne(oe(import.meta.url))}var de=ae(),I=process.env.CLAUDE_MEM_DATA_DIR||T(j(),".claude-mem"),U=process.env.CLAUDE_CONFIG_DIR||T(j(),".claude"),ve=T(I,"archives"),Ae=T(I,"logs"),ye=T(I,"trash"),Ce=T(I,"backups"),De=T(I,"settings.json"),Y=T(I,"claude-mem.db"),ke=T(U,"settings.json"),xe=T(U,"commands"),we=T(U,"CLAUDE.md");function K(a){ie(a,{recursive:!0})}function V(){return T(de,"..","..")}var $=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))($||{}),M=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=$[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,o){if(e<this.level)return;let c=new Date().toISOString().replace("T"," ").substring(0,23),p=$[e].padEnd(5),u=t.padEnd(6),O="";r?.correlationId?O=`[${r.correlationId}] `:r?.sessionId&&(O=`[session-${r.sessionId}] `);let b="";o!=null&&(this.level===0&&typeof o=="object"?b=`
|
||||
`+JSON.stringify(o,null,2):b=" "+this.formatData(o));let n="";if(r){let{sessionId:h,sdkSessionId:k,correlationId:L,...C}=r;Object.keys(C).length>0&&(n=` {${Object.entries(C).map(([d,m])=>`${d}=${m}`).join(", ")}}`)}let N=`[${c}] [${p}] [${u}] ${O}${s}${n}${b}`;e===3?console.error(N):console.log(N)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},q=new M;var D=class{db;constructor(){K(I),this.db=new ce(Y),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(s=>s.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(u=>u.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -99,7 +99,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let s=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!s||s.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -174,7 +174,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
`).all(e,t)}getRecentSummariesWithSessionInfo(e,t=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
@@ -182,13 +182,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
`).all(e,t)}getRecentSessionsWithStatus(e,t=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
@@ -205,7 +205,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
`).all(e,t)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
@@ -218,11 +218,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
`).get(e)||null}getFilesForSession(e){let s=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,i=new Set;for(let d of t){if(d.files_read)try{let n=JSON.parse(d.files_read);Array.isArray(n)&&n.forEach(c=>r.add(c))}catch{}if(d.files_modified)try{let n=JSON.parse(d.files_modified);Array.isArray(n)&&n.forEach(c=>i.add(c))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,o=new Set;for(let c of s){if(c.files_read)try{let p=JSON.parse(c.files_read);Array.isArray(p)&&p.forEach(u=>r.add(u))}catch{}if(c.files_modified)try{let p=JSON.parse(c.files_modified);Array.isArray(p)&&p.forEach(u=>o.add(u))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -237,11 +237,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
@@ -249,82 +249,83 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,i=r.getTime(),n=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){let r=new Date,o=r.getTime(),p=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),i);return n.lastInsertRowid===0||n.changes===0?this.db.prepare(`
|
||||
`).run(e,e,t,s,r.toISOString(),o);return p.lastInsertRowid===0||p.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:n.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e).id:p.lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(X.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(t,e).changes===0?(q.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,i=r.getTime();return this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,t,s){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),i).lastInsertRowid}storeObservation(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
`).run(e,t,s,r.toISOString(),o).lastInsertRowid}storeObservation(e,t,s,r){let o=new Date,c=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
`).run(e,e,t,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d)}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
`).run(e,t,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),r||null,o.toISOString(),c)}storeSummary(e,t,s,r){let o=new Date,c=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
`).run(e,e,t,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),d)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(e,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,r||null,o.toISOString(),c)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import v from"path";import{existsSync as y}from"fs";import{spawn as K}from"child_process";var V=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),J=`http://127.0.0.1:${V}/health`;async function F(){try{return(await fetch(J,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function P(){try{if(await F())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=M(),e=v.join(p,"plugin","scripts","worker-service.cjs");if(!y(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=v.join(p,"ecosystem.config.cjs"),t=v.join(p,"node_modules",".bin","pm2");if(!y(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!y(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=K(t,["start",s],{detached:!0,stdio:"ignore",cwd:p});r.on("error",i=>{throw new Error(`Failed to spawn PM2: ${i.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let i=0;i<3;i++)if(await new Promise(d=>setTimeout(d,500)),await F())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m"};function D(p,e=!1,s=!1){P();let t=p?.cwd??process.cwd(),r=t?C.basename(t):"unknown-project",i=new N;try{let d=i.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(r);if(d.length===0)return e?`
|
||||
${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}
|
||||
${o.gray}${"\u2500".repeat(60)}${o.reset}
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};import X from"path";import{existsSync as F}from"fs";import{spawn as pe}from"child_process";var ue=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),le=`http://127.0.0.1:${ue}/health`;async function J(){try{return(await fetch(le,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function Q(){try{if(await J())return!0;console.error("[claude-mem] Worker not responding, starting...");let a=V(),e=X.join(a,"plugin","scripts","worker-service.cjs");if(!F(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=X.join(a,"ecosystem.config.cjs"),s=X.join(a,"node_modules",".bin","pm2");if(!F(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!F(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let r=pe(s,["start",t],{detached:!0,stdio:"ignore",cwd:a});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(c=>setTimeout(c,500)),await J())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(a){return console.error(`[claude-mem] Failed to start worker: ${a.message}`),!1}}var z=8,i={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function G(a){if(!a)return[];let e=JSON.parse(a);return Array.isArray(e)?e:[]}function me(a){return new Date(a).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function _e(a){return new Date(a).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function Ee(a){return new Date(a).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Te(a){return a?Math.ceil(a.length/4):0}function he(a,e){return W.isAbsolute(a)?W.relative(e,a):a}function ge(a,e){if(e.length===0)return[];let t=e.map(()=>"?").join(",");return a.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
facts, concepts, files_read, files_modified,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
WHERE sdk_session_id IN (${t})
|
||||
ORDER BY created_at_epoch DESC
|
||||
`).all(...e)}function Z(a,e=!1,t=!1){Q();let s=a?.cwd??process.cwd(),r=s?W.basename(s):"unknown-project",o=new D,c=o.db.prepare(`
|
||||
SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(r,z+1);if(c.length===0)return o.close(),e?`
|
||||
${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}
|
||||
${i.gray}${"\u2500".repeat(60)}${i.reset}
|
||||
|
||||
${o.dim}No previous summaries found for this project yet.${o.reset}
|
||||
${i.dim}No previous sessions found for this project yet.${i.reset}
|
||||
`:`# [${r}] recent context
|
||||
|
||||
No previous summaries found for this project yet.`;let n=[];e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)):(n.push(`# [${r}] recent context`),n.push(""));let c=!0;for(let E=0;E<d.length;E++){let a=d[E],l=d.length-1-E,f=l===0,G=l>=1&&l<=3,k=l>3;if(c?e&&n.push(""):e?(n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push("---"),n.push("")),c=!1,k){a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push("")));let T=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${T}${o.reset}`):(n.push(`**Date:** ${T}`),n.push(""));continue}if(a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push(""))),f&&a.learned&&(e?(n.push(`${o.bright}${o.blue}Learned:${o.reset} ${a.learned}`),n.push("")):(n.push(`**Learned:** ${a.learned}`),n.push(""))),a.completed&&(e?(n.push(`${o.bright}${o.green}Completed:${o.reset} ${a.completed}`),n.push("")):(n.push(`**Completed:** ${a.completed}`),n.push(""))),f&&a.next_steps&&(e?(n.push(`${o.bright}${o.magenta}Next Steps:${o.reset} ${a.next_steps}`),n.push("")):(n.push(`**Next Steps:** ${a.next_steps}`),n.push(""))),f){let T=i.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(a.sdk_session_id),h=new Set,g=new Set,b=u=>{try{return C.isAbsolute(u)?C.relative(t,u):u}catch{return u}};for(let u of T){if(u.files_read)try{let S=JSON.parse(u.files_read);Array.isArray(S)&&S.forEach(I=>h.add(b(I)))}catch{}if(u.files_modified)try{let S=JSON.parse(u.files_modified);Array.isArray(S)&&S.forEach(I=>g.add(b(I)))}catch{}}g.forEach(u=>h.delete(u)),h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),g.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(g).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(g).join(", ")}`))}let R=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${R}${o.reset}`):n.push(`**Date:** ${R}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(`
|
||||
`)}finally{i.close()}}import{stdin as x}from"process";try{let p=process.argv.includes("--index");if(x.isTTY){let e=D(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";x.on("data",s=>e+=s),x.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:D(s,!1,p)}};console.log(JSON.stringify(r)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
|
||||
No previous sessions found for this project yet.`;let p=c.slice(0,z),u=[...new Set(p.map(N=>N.sdk_session_id))],b=ge(o,u).filter(N=>{let h=G(N.concepts);return h.includes("what-changed")||h.includes("how-it-works")||h.includes("problem-solution")||h.includes("gotcha")||h.includes("discovery")||h.includes("why-it-exists")||h.includes("decision")||h.includes("trade-off")}),n=[];if(e?(n.push(""),n.push(`${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}`),n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),b.length>0){e?(n.push(`${i.dim}Legend: \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off${i.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off"),n.push("")),e?(n.push(`${i.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${i.reset}`),n.push(`${i.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${i.reset}`),n.push(`${i.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${i.reset}`),n.push(`${i.dim} \u2192 Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) often worth fetching immediately${i.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) often worth fetching immediately"),n.push(""));let N=c[0]?.id,h=p.map((d,m)=>{let l=m===0?null:c[m+1];return{...d,displayEpoch:l?l.created_at_epoch:d.created_at_epoch,displayTime:l?l.created_at:d.created_at,isMostRecent:d.id===N}}),k=[...b.map(d=>({type:"observation",data:d})),...h.map(d=>({type:"summary",data:d}))];k.sort((d,m)=>{let l=d.type==="observation"?d.data.created_at_epoch:d.data.displayEpoch,R=m.type==="observation"?m.data.created_at_epoch:m.data.displayEpoch;return l-R});let L=new Map;for(let d of k){let m=d.type==="observation"?d.data.created_at:d.data.displayTime,l=Ee(m);L.has(l)||L.set(l,[]),L.get(l).push(d)}let C=Array.from(L.entries()).sort((d,m)=>{let l=new Date(d[0]).getTime(),R=new Date(m[0]).getTime();return l-R});for(let[d,m]of C){e?(n.push(`${i.bright}${i.cyan}${d}${i.reset}`),n.push("")):(n.push(`### ${d}`),n.push(""));let l=null,R="",v=!1;for(let x of m)if(x.type==="summary"){v&&(n.push(""),v=!1,l=null,R="");let _=x.data,A=`${_.request||"Session started"} (${me(_.displayTime)})`,S=_.isMostRecent?"":`claude-mem://session-summary/${_.id}`;if(e){let E=S?`${i.dim}[${S}]${i.reset}`:"";n.push(`\u{1F3AF} ${i.yellow}#S${_.id}${i.reset} ${A} ${E}`)}else{let E=S?` [\u2192](${S})`:"";n.push(`**\u{1F3AF} #S${_.id}** ${A}${E}`)}n.push("")}else{let _=x.data,A=G(_.files_modified),S=A.length>0?he(A[0],s):"General";S!==l&&(v&&n.push(""),e?n.push(`${i.dim}${S}${i.reset}`):n.push(`**${S}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),l=S,v=!0,R="");let E=G(_.concepts),f="\u2022";E.includes("gotcha")?f="\u{1F534}":E.includes("decision")?f="\u{1F7E4}":E.includes("trade-off")?f="\u2696\uFE0F":E.includes("problem-solution")?f="\u{1F7E1}":E.includes("discovery")?f="\u{1F7E3}":E.includes("why-it-exists")?f="\u{1F7E0}":E.includes("how-it-works")?f="\u{1F535}":E.includes("what-changed")&&(f="\u{1F7E2}");let y=_e(_.created_at),H=_.title||"Untitled",w=Te(_.narrative),B=y!==R,se=B?y:"";if(R=y,e){let te=B?`${i.dim}${y}${i.reset}`:" ".repeat(y.length),re=w>0?`${i.dim}(~${w}t)${i.reset}`:"";n.push(` ${i.dim}#${_.id}${i.reset} ${te} ${f} ${H} ${re}`)}else n.push(`| #${_.id} | ${se||"\u2033"} | ${f} | ${H} | ~${w} |`)}v&&n.push("")}let g=c[0];g&&(g.completed||g.next_steps)&&(g.completed&&(e?n.push(`${i.green}Completed:${i.reset} ${g.completed}`):n.push(`**Completed**: ${g.completed}`),n.push("")),g.next_steps&&(e?n.push(`${i.magenta}Next Steps:${i.reset} ${g.next_steps}`):n.push(`**Next Steps**: ${g.next_steps}`),n.push(""))),e?n.push(`${i.dim}Use claude-mem MCP search to access records with the given ID${i.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return o.close(),n.join(`
|
||||
`).trimEnd()}var ee=process.argv.includes("--index"),fe=process.argv.includes("--colors");if(P.isTTY||fe){let a=Z(void 0,!0,ee);console.log(a),process.exit(0)}else{let a="";P.on("data",e=>a+=e),P.on("end",()=>{let e=a.trim()?JSON.parse(a):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:Z(e,!1,ee)}};console.log(JSON.stringify(s)),process.exit(0)})}
|
||||
|
||||
+13
-13
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import Y from"path";import W from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var G=H(),u=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),oe=p(u,"archives"),ne=p(u,"logs"),ie=p(u,"trash"),ae=p(u,"backups"),de=p(u,"settings.json"),O=p(u,"claude-mem.db"),pe=p(l,"settings.json"),ce=p(l,"commands"),Ee=p(l,"CLAUDE.md");function I(n){P(n,{recursive:!0})}function L(){return p(G,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
|
||||
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:V,correlationId:q,...f}=r;Object.keys(f).length>0&&(_=` {${Object.entries(f).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${_}${c}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new S;var m=class{db;constructor(){I(u),this.db=new W(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import j from"path";import{stdin as x}from"process";import G from"better-sqlite3";import{join as p,dirname as X,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as M}from"fs";import{fileURLToPath as P}from"url";function F(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var H=F(),u=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),ne=p(u,"archives"),oe=p(u,"logs"),ie=p(u,"trash"),ae=p(u,"backups"),de=p(u,"settings.json"),O=p(u,"claude-mem.db"),pe=p(l,"settings.json"),ce=p(l,"commands"),Ee=p(l,"CLAUDE.md");function I(o){M(o,{recursive:!0})}function L(){return p(H,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let c="";n!=null&&(this.level===0&&typeof n=="object"?c=`
|
||||
`+JSON.stringify(n,null,2):c=" "+this.formatData(n));let _="";if(r){let{sessionId:K,sdkSessionId:V,correlationId:q,...f}=r;Object.keys(f).length>0&&(_=` {${Object.entries(f).map(([U,w])=>`${U}=${w}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${_}${c}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new S;var m=class{db;constructor(){I(u),this.db=new G(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -222,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -249,11 +249,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
@@ -268,33 +268,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -306,4 +306,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function B(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=B(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as b}from"fs";import{spawn as $}from"child_process";var k=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${k}/health`;async function C(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=L(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}function y(){return k}async function x(n){if(!n)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=n,r=Y.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new m;try{let a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);i.saveUserPrompt(e,d,t),console.error(`[new-hook] Session ${a}, prompt #${d}`);let E=y(),c=await fetch(`http://127.0.0.1:${E}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var N="";U.on("data",n=>N+=n);U.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await x(n),process.exit(0)});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=W(o,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as b}from"fs";import{spawn as B}from"child_process";var k=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${k}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(o,"ecosystem.config.cjs"),t=g.join(o,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=B(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}function y(){return k}async function Y(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=j.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new m,a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);i.saveUserPrompt(e,d,t),console.error(`[new-hook] Session ${a}, prompt #${d}`),i.close();let E=y(),c=await fetch(`http://127.0.0.1:${E}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}console.log(v("UserPromptSubmit",!0))}var N="";x.on("data",o=>N+=o);x.on("end",async()=>{let o=N?JSON.parse(N):void 0;await Y(o)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as M}from"fs";import{fileURLToPath as X}from"url";function P(){return typeof __dirname<"u"?__dirname:w(X(import.meta.url))}var F=P(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),S=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(S,"settings.json"),pe=p(S,"commands"),ce=p(S,"CLAUDE.md");function A(n){M(n,{recursive:!0})}function v(){return p(F,"..","..")}var g=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import{stdin as D}from"process";import F from"better-sqlite3";import{join as p,dirname as U,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as w}from"fs";import{fileURLToPath as M}from"url";function X(){return typeof __dirname<"u"?__dirname:U(M(import.meta.url))}var P=X(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),S=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(S,"settings.json"),pe=p(S,"commands"),ce=p(S,"CLAUDE.md");function A(n){w(n,{recursive:!0})}function v(){return p(P,"..","..")}var g=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=g[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:V,...h}=r;Object.keys(h).length>0&&(_=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let l=`[${i}] [${a}] [${d}] ${c}${t}${_}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},m=new b;var T=class{db;constructor(){A(u),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:V,...h}=r;Object.keys(h).length>0&&(_=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let l=`[${i}] [${a}] [${d}] ${c}${t}${_}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},m=new b;var T=class{db;constructor(){A(u),this.db=new F(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -306,4 +306,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function N(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as f}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=v(),e=R.join(n,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(n,"ecosystem.config.cjs"),t=R.join(n,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var j=new Set(["ListMcpResourcesTool"]);async function D(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(j.has(s)){console.log(N("PostToolUse",!0));return}if(!await k())throw new Error("Worker service failed to start or become healthy");let i=new T,a=i.createSDKSession(e,"",""),d=i.getPromptCounter(a);i.close();let c=m.formatTool(s,t),E=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);m.dataIn("HOOK",`PostToolUse: ${c}`,{sessionId:a,workerPort:E});let _=await fetch(`http://127.0.0.1:${E}/sessions/${a}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!_.ok){let l=await _.text();throw m.failure("HOOK","Failed to send observation",{sessionId:a,status:_.status},l),new Error(`Failed to send observation to worker: ${_.status} ${l}`)}m.debug("HOOK","Observation sent successfully",{sessionId:a,toolName:s}),console.log(N("PostToolUse",!0))}import{stdin as y}from"process";var O="";y.on("data",n=>O+=n);y.on("end",async()=>{let n=O.trim()?JSON.parse(O):void 0;await D(n),process.exit(0)});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function H(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function N(n,e,s={}){let t=H(n,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as f}from"fs";import{spawn as G}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${W}/health`;async function C(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=v(),e=R.join(n,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(n,"ecosystem.config.cjs"),t=R.join(n,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var $=new Set(["ListMcpResourcesTool"]);async function j(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if($.has(s)){console.log(N("PostToolUse",!0));return}if(!await k())throw new Error("Worker service failed to start or become healthy");let i=new T,a=i.createSDKSession(e,"",""),d=i.getPromptCounter(a);i.close();let c=m.formatTool(s,t),E=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);m.dataIn("HOOK",`PostToolUse: ${c}`,{sessionId:a,workerPort:E});let _=await fetch(`http://127.0.0.1:${E}/sessions/${a}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!_.ok){let l=await _.text();throw m.failure("HOOK","Failed to send observation",{sessionId:a,status:_.status},l),new Error(`Failed to send observation to worker: ${_.status} ${l}`)}m.debug("HOOK","Observation sent successfully",{sessionId:a,toolName:s}),console.log(N("PostToolUse",!0))}var O="";D.on("data",n=>O+=n);D.on("end",async()=>{let n=O?JSON.parse(O):void 0;await j(n)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),oe=p(c,"trash"),ne=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(l,"settings.json"),de=p(l,"commands"),pe=p(l,"CLAUDE.md");function L(n){X(n,{recursive:!0})}function A(){return p(F,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
|
||||
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let b="";if(r){let{sessionId:j,sdkSessionId:K,correlationId:Y,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let f=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(f):console.log(f)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new S;var m=class{db;constructor(){L(c),this.db=new H(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as D}from"process";import F from"better-sqlite3";import{join as p,dirname as U,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var P=M(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),ne=p(c,"trash"),oe=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(l,"settings.json"),de=p(l,"commands"),pe=p(l,"CLAUDE.md");function L(o){w(o,{recursive:!0})}function A(){return p(P,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let b="";if(r){let{sessionId:j,sdkSessionId:K,correlationId:Y,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let f=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(f):console.log(f)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new S;var m=class{db;constructor(){L(c),this.db=new F(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -222,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -249,11 +249,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
@@ -268,33 +268,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -306,4 +306,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as R}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!R(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!R(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!R(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}async function D(n){if(!n)throw new Error("summaryHook requires input");let{session_id:e}=n;if(!await k())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.createSDKSession(e,"",""),o=t.getPromptCounter(r);t.close();let i=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:i,promptNumber:o});let a=await fetch(`http://127.0.0.1:${i}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:o}),signal:AbortSignal.timeout(2e3)});if(!a.ok){let d=await a.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:a.status},d),new Error(`Failed to request summary from worker: ${a.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}import{stdin as y}from"process";var N="";y.on("data",n=>N+=n);y.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await D(n),process.exit(0)});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function H(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=H(o,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as R}from"fs";import{spawn as G}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${W}/health`;async function C(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=A(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!R(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(o,"ecosystem.config.cjs"),t=g.join(o,"node_modules",".bin","pm2");if(!R(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!R(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function $(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o;if(!await k())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.createSDKSession(e,"",""),n=t.getPromptCounter(r);t.close();let i=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:i,promptNumber:n});let a=await fetch(`http://127.0.0.1:${i}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:n}),signal:AbortSignal.timeout(2e3)});if(!a.ok){let d=await a.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:a.status},d),new Error(`Failed to request summary from worker: ${a.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}var N="";D.on("data",o=>N+=o);D.on("end",async()=>{let o=N?JSON.parse(N):void 0;await $(o)});
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
import{execSync as r}from"child_process";import{join as o}from"path";import{homedir as t}from"os";import{existsSync as s}from"fs";var i=o(t(),".claude","plugins","marketplaces","thedotmack"),a=o(i,"node_modules");s(a)||(console.error(`
|
||||
---
|
||||
\u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
|
||||
user messages in Claude Code UI until a better method is provided.
|
||||
---
|
||||
|
||||
\u26A0\uFE0F Claude-Mem: First-Time Setup
|
||||
|
||||
Dependencies have been installed in the background. This only happens once.
|
||||
|
||||
\u{1F4A1} TIPS:
|
||||
\u2022 Memories will start generating while you work
|
||||
\u2022 Use /init to write or update your CLAUDE.md for better project context
|
||||
\u2022 Try /clear after one session to see what context looks like
|
||||
|
||||
Thank you for installing Claude-Mem!
|
||||
|
||||
This message was not added to your startup context, so you can continue working as normal.
|
||||
`),process.exit(3));try{let e=o(t(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),n=r(`node "${e}" --colors`,{encoding:"utf8"});console.error(`
|
||||
|
||||
\u{1F4DD} Claude-Mem Context Loaded
|
||||
\u2139\uFE0F Note: This appears as stderr but is informational only
|
||||
|
||||
`+n)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
|
||||
File diff suppressed because one or more lines are too long
@@ -13,11 +13,12 @@ import { fileURLToPath } from 'url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const HOOKS = [
|
||||
{ name: 'context-hook', source: 'src/bin/hooks/context-hook.ts' },
|
||||
{ name: 'new-hook', source: 'src/bin/hooks/new-hook.ts' },
|
||||
{ name: 'save-hook', source: 'src/bin/hooks/save-hook.ts' },
|
||||
{ name: 'summary-hook', source: 'src/bin/hooks/summary-hook.ts' },
|
||||
{ name: 'cleanup-hook', source: 'src/bin/hooks/cleanup-hook.ts' }
|
||||
{ name: 'context-hook', source: 'src/hooks/context-hook.ts' },
|
||||
{ name: 'new-hook', source: 'src/hooks/new-hook.ts' },
|
||||
{ name: 'save-hook', source: 'src/hooks/save-hook.ts' },
|
||||
{ name: 'summary-hook', source: 'src/hooks/summary-hook.ts' },
|
||||
{ name: 'cleanup-hook', source: 'src/hooks/cleanup-hook.ts' },
|
||||
{ name: 'user-message-hook', source: 'src/hooks/user-message-hook.ts' }
|
||||
];
|
||||
|
||||
const WORKER_SERVICE = {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
/**
|
||||
* Cleanup Hook Entry Point - SessionEnd
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { cleanupHook } from '../../hooks/cleanup.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await cleanupHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem cleanup-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
/**
|
||||
* Context Hook Entry Point - SessionStart
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { contextHook } from '../../hooks/context.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
try {
|
||||
// Check for --index flag
|
||||
const useIndexView = process.argv.includes('--index');
|
||||
|
||||
if (stdin.isTTY) {
|
||||
// Running manually from terminal - print formatted output with colors
|
||||
const contextOutput = contextHook(undefined, true, useIndexView);
|
||||
console.log(contextOutput);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Running from hook - wrap in JSON format without colors
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
const contextOutput = contextHook(parsed, false, useIndexView);
|
||||
const result = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: contextOutput
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(result));
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem context-hook error: ${error.message}]`);
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
/**
|
||||
* New Hook Entry Point - UserPromptSubmit
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { newHook } from '../../hooks/new.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await newHook(parsed);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
/**
|
||||
* Save Hook Entry Point - PostToolUse
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { saveHook } from '../../hooks/save.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await saveHook(parsed);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
/**
|
||||
* Summary Hook Entry Point - Stop
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { summaryHook } from '../../hooks/summary.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await summaryHook(parsed);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ function buildTimestampMap(): TimestampMapping {
|
||||
const data = JSON.parse(line);
|
||||
const timestamp = data.timestamp;
|
||||
const sessionId = data.sessionId;
|
||||
const project = data.cwd || '/Users/alexnewman/Scripts/claude-mem';
|
||||
const project = data.cwd;
|
||||
|
||||
if (timestamp && sessionId) {
|
||||
// Round timestamp to second for matching with XML timestamps
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Cleanup Hook - SessionEnd
|
||||
* Consolidated entry point + logic
|
||||
*/
|
||||
|
||||
import { stdin } from 'process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
transcript_path?: string;
|
||||
hook_event_name: string;
|
||||
reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Hook Main Logic
|
||||
*/
|
||||
async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
// Log hook entry point
|
||||
console.error('[claude-mem cleanup] Hook fired', {
|
||||
input: input ? {
|
||||
session_id: input.session_id,
|
||||
cwd: input.cwd,
|
||||
reason: input.reason
|
||||
} : null
|
||||
});
|
||||
|
||||
// Handle standalone execution (no input provided)
|
||||
if (!input) {
|
||||
console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook');
|
||||
console.log('\nExpected input format:');
|
||||
console.log(JSON.stringify({
|
||||
session_id: "string",
|
||||
cwd: "string",
|
||||
transcript_path: "string",
|
||||
hook_event_name: "SessionEnd",
|
||||
reason: "exit"
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { session_id, reason } = input;
|
||||
console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason });
|
||||
|
||||
// Ensure worker is running first
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
|
||||
}
|
||||
|
||||
// Find active SDK session
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
// No active session - nothing to clean up
|
||||
console.error('[claude-mem cleanup] No active SDK session found', { session_id });
|
||||
db.close();
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error('[claude-mem cleanup] Active SDK session found', {
|
||||
session_id: session.id,
|
||||
sdk_session_id: session.sdk_session_id,
|
||||
project: session.project,
|
||||
worker_port: session.worker_port
|
||||
});
|
||||
|
||||
// Mark session as completed in DB
|
||||
db.markSessionCompleted(session.id);
|
||||
console.error('[claude-mem cleanup] Session marked as completed in database');
|
||||
|
||||
db.close();
|
||||
|
||||
console.error('[claude-mem cleanup] Cleanup completed successfully');
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Entry Point
|
||||
if (stdin.isTTY) {
|
||||
// Running manually
|
||||
cleanupHook(undefined);
|
||||
} else {
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input ? JSON.parse(input) : undefined;
|
||||
await cleanupHook(parsed);
|
||||
});
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
transcript_path?: string;
|
||||
hook_event_name: string;
|
||||
reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Hook - SessionEnd
|
||||
* Marks session as completed when Claude Code session ends
|
||||
*
|
||||
* This hook runs when a Claude Code session ends. It:
|
||||
* 1. Finds active SDK session for this Claude session
|
||||
* 2. Marks session as completed in database
|
||||
* 3. Allows worker to finish pending operations naturally
|
||||
*/
|
||||
export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
try {
|
||||
// Log hook entry point
|
||||
console.error('[claude-mem cleanup] Hook fired', {
|
||||
input: input ? {
|
||||
session_id: input.session_id,
|
||||
cwd: input.cwd,
|
||||
reason: input.reason
|
||||
} : null
|
||||
});
|
||||
|
||||
// Handle standalone execution (no input provided)
|
||||
if (!input) {
|
||||
console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook');
|
||||
console.log('\nExpected input format:');
|
||||
console.log(JSON.stringify({
|
||||
session_id: "string",
|
||||
cwd: "string",
|
||||
transcript_path: "string",
|
||||
hook_event_name: "SessionEnd",
|
||||
reason: "exit"
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { session_id, reason } = input;
|
||||
console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason });
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
|
||||
}
|
||||
|
||||
// Find active SDK session
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
// No active session - nothing to clean up
|
||||
console.error('[claude-mem cleanup] No active SDK session found', { session_id });
|
||||
db.close();
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error('[claude-mem cleanup] Active SDK session found', {
|
||||
session_id: session.id,
|
||||
sdk_session_id: session.sdk_session_id,
|
||||
project: session.project,
|
||||
worker_port: session.worker_port
|
||||
});
|
||||
|
||||
// 1. Mark session as completed in DB (if not already completed)
|
||||
try {
|
||||
db.markSessionCompleted(session.id);
|
||||
console.error('[claude-mem cleanup] Session marked as completed in database');
|
||||
} catch (markErr: any) {
|
||||
console.error('[claude-mem cleanup] Failed to mark session as completed:', markErr);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
console.error('[claude-mem cleanup] Cleanup completed successfully');
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error: any) {
|
||||
// On error, don't block Claude Code exit
|
||||
console.error('[claude-mem cleanup] Unexpected error in hook', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
});
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
/**
|
||||
* Context Hook - SessionStart
|
||||
* Consolidated entry point + logic
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { stdin } from 'process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
// Configuration: Number of sessions to display in context
|
||||
const DISPLAY_SESSION_COUNT = 8;
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
transcript_path?: string;
|
||||
cwd?: string;
|
||||
hook_event_name?: string;
|
||||
source?: "startup" | "resume" | "clear" | "compact";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// ANSI color codes for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
gray: '\x1b[90m',
|
||||
red: '\x1b[31m',
|
||||
};
|
||||
|
||||
interface Observation {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
type: string;
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
narrative: string | null;
|
||||
facts: string | null;
|
||||
concepts: string | null;
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
// Helper: Parse JSON array safely
|
||||
function parseJsonArray(json: string | null): string[] {
|
||||
if (!json) return [];
|
||||
const parsed = JSON.parse(json);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
|
||||
// Helper: Format date with time
|
||||
function formatDateTime(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Format just time (no date)
|
||||
function formatTime(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Format just date
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Estimate token count for text
|
||||
function estimateTokens(text: string | null): number {
|
||||
if (!text) return 0;
|
||||
// Rough estimate: ~4 characters per token
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
// Helper: Convert absolute paths to relative paths
|
||||
function toRelativePath(filePath: string, cwd: string): string {
|
||||
if (path.isAbsolute(filePath)) {
|
||||
return path.relative(cwd, filePath);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// Helper: Get all observations for given sessions
|
||||
function getObservations(db: SessionStore, sessionIds: string[]): Observation[] {
|
||||
if (sessionIds.length === 0) return [];
|
||||
|
||||
const placeholders = sessionIds.map(() => '?').join(',');
|
||||
const observations = db.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
facts, concepts, files_read, files_modified,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
WHERE sdk_session_id IN (${placeholders})
|
||||
ORDER BY created_at_epoch DESC
|
||||
`).all(...sessionIds) as Observation[];
|
||||
|
||||
return observations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context Hook Main Logic
|
||||
*/
|
||||
function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string {
|
||||
ensureWorkerRunning();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get last N summaries (use N+1 for offset calculation)
|
||||
const recentSummaries = db.db.prepare(`
|
||||
SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(project, DISPLAY_SESSION_COUNT + 1) as Array<{ id: number; sdk_session_id: string; request: string | null; completed: string | null; next_steps: string | null; created_at: string; created_at_epoch: number }>;
|
||||
|
||||
if (recentSummaries.length === 0) {
|
||||
db.close();
|
||||
if (useColors) {
|
||||
return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous sessions found for this project yet.${colors.reset}\n`;
|
||||
}
|
||||
return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`;
|
||||
}
|
||||
|
||||
// Extract unique session IDs from first N summaries
|
||||
const displaySummaries = recentSummaries.slice(0, DISPLAY_SESSION_COUNT);
|
||||
const sessionIds = [...new Set(displaySummaries.map(s => s.sdk_session_id))];
|
||||
|
||||
// Get all observations from these sessions
|
||||
const observations = getObservations(db, sessionIds);
|
||||
|
||||
// Filter observations by key concepts for timeline
|
||||
const timelineObs = observations.filter(obs => {
|
||||
const concepts = parseJsonArray(obs.concepts);
|
||||
return concepts.includes('what-changed') ||
|
||||
concepts.includes('how-it-works') ||
|
||||
concepts.includes('problem-solution') ||
|
||||
concepts.includes('gotcha') ||
|
||||
concepts.includes('discovery') ||
|
||||
concepts.includes('why-it-exists') ||
|
||||
concepts.includes('decision') ||
|
||||
concepts.includes('trade-off');
|
||||
});
|
||||
|
||||
// Build output
|
||||
const output: string[] = [];
|
||||
|
||||
// Header
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`# [${project}] recent context`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Chronological Timeline
|
||||
if (timelineObs.length > 0) {
|
||||
// Legend/Key
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Legend:** 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Progressive Disclosure Usage Instructions
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}💡 Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${colors.reset}`);
|
||||
output.push(`${colors.dim} → Use MCP search tools to fetch full observation details on-demand (Layer 2)${colors.reset}`);
|
||||
output.push(`${colors.dim} → Prefer searching observations over re-reading code for past decisions and learnings${colors.reset}`);
|
||||
output.push(`${colors.dim} → Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`💡 **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts).`);
|
||||
output.push(`- Use MCP search tools to fetch full observation details on-demand (Layer 2)`);
|
||||
output.push(`- Prefer searching observations over re-reading code for past decisions and learnings`);
|
||||
output.push(`- Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Create unified timeline with both observations and summaries
|
||||
const mostRecentSummaryId = recentSummaries[0]?.id;
|
||||
|
||||
// Create offset summaries
|
||||
const summariesWithOffset = displaySummaries.map((summary, i) => {
|
||||
// Most recent keeps its own time, others offset to next summary's time
|
||||
const nextSummary = i === 0 ? null : recentSummaries[i + 1];
|
||||
return {
|
||||
...summary,
|
||||
displayEpoch: nextSummary ? nextSummary.created_at_epoch : summary.created_at_epoch,
|
||||
displayTime: nextSummary ? nextSummary.created_at : summary.created_at,
|
||||
isMostRecent: summary.id === mostRecentSummaryId
|
||||
};
|
||||
});
|
||||
|
||||
type TimelineItem =
|
||||
| { type: 'observation'; data: Observation }
|
||||
| { type: 'summary'; data: typeof summariesWithOffset[0] };
|
||||
|
||||
const timeline: TimelineItem[] = [
|
||||
...timelineObs.map(obs => ({ type: 'observation' as const, data: obs })),
|
||||
...summariesWithOffset.map(summary => ({ type: 'summary' as const, data: summary }))
|
||||
];
|
||||
|
||||
// Sort chronologically
|
||||
timeline.sort((a, b) => {
|
||||
const aEpoch = a.type === 'observation' ? a.data.created_at_epoch : a.data.displayEpoch;
|
||||
const bEpoch = b.type === 'observation' ? b.data.created_at_epoch : b.data.displayEpoch;
|
||||
return aEpoch - bEpoch;
|
||||
});
|
||||
|
||||
// Group by day for rendering
|
||||
const dayTimelines = new Map<string, typeof timeline>();
|
||||
for (const item of timeline) {
|
||||
const itemDate = item.type === 'observation' ? item.data.created_at : item.data.displayTime;
|
||||
const day = formatDate(itemDate);
|
||||
if (!dayTimelines.has(day)) {
|
||||
dayTimelines.set(day, []);
|
||||
}
|
||||
dayTimelines.get(day)!.push(item);
|
||||
}
|
||||
|
||||
// Sort days chronologically
|
||||
const sortedDays = Array.from(dayTimelines.entries()).sort((a, b) => {
|
||||
const aDate = new Date(a[0]).getTime();
|
||||
const bDate = new Date(b[0]).getTime();
|
||||
return aDate - bDate;
|
||||
});
|
||||
|
||||
// Render each day's timeline
|
||||
for (const [day, dayItems] of sortedDays) {
|
||||
// Day header
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.cyan}${day}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`### ${day}`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// Render items chronologically with visual file grouping
|
||||
let currentFile: string | null = null;
|
||||
let lastTime = '';
|
||||
let tableOpen = false;
|
||||
|
||||
for (const item of dayItems) {
|
||||
if (item.type === 'summary') {
|
||||
// Close any open table
|
||||
if (tableOpen) {
|
||||
output.push('');
|
||||
tableOpen = false;
|
||||
currentFile = null;
|
||||
lastTime = '';
|
||||
}
|
||||
|
||||
// Render summary
|
||||
const summary = item.data;
|
||||
const summaryTitle = `${summary.request || 'Session started'} (${formatDateTime(summary.displayTime)})`;
|
||||
const link = summary.isMostRecent ? '' : `claude-mem://session-summary/${summary.id}`;
|
||||
|
||||
if (useColors) {
|
||||
const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : '';
|
||||
output.push(`🎯 ${colors.yellow}#S${summary.id}${colors.reset} ${summaryTitle} ${linkPart}`);
|
||||
} else {
|
||||
const linkPart = link ? ` [→](${link})` : '';
|
||||
output.push(`**🎯 #S${summary.id}** ${summaryTitle}${linkPart}`);
|
||||
}
|
||||
output.push('');
|
||||
} else {
|
||||
// Render observation
|
||||
const obs = item.data;
|
||||
const files = parseJsonArray(obs.files_modified);
|
||||
const file = files.length > 0 ? toRelativePath(files[0], cwd) : 'General';
|
||||
|
||||
// Check if we need a new file section
|
||||
if (file !== currentFile) {
|
||||
// Close previous table
|
||||
if (tableOpen) {
|
||||
output.push('');
|
||||
}
|
||||
|
||||
// File header
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}${file}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**${file}**`);
|
||||
}
|
||||
|
||||
// Table header (markdown only)
|
||||
if (!useColors) {
|
||||
output.push(`| ID | Time | T | Title | Tokens |`);
|
||||
output.push(`|----|------|---|-------|--------|`);
|
||||
}
|
||||
|
||||
currentFile = file;
|
||||
tableOpen = true;
|
||||
lastTime = '';
|
||||
}
|
||||
|
||||
// Render observation row
|
||||
const concepts = parseJsonArray(obs.concepts);
|
||||
let icon = '•';
|
||||
|
||||
// Priority order: gotcha > decision > trade-off > problem-solution > discovery > why-it-exists > how-it-works > what-changed
|
||||
if (concepts.includes('gotcha')) {
|
||||
icon = '🔴';
|
||||
} else if (concepts.includes('decision')) {
|
||||
icon = '🟤';
|
||||
} else if (concepts.includes('trade-off')) {
|
||||
icon = '⚖️';
|
||||
} else if (concepts.includes('problem-solution')) {
|
||||
icon = '🟡';
|
||||
} else if (concepts.includes('discovery')) {
|
||||
icon = '🟣';
|
||||
} else if (concepts.includes('why-it-exists')) {
|
||||
icon = '🟠';
|
||||
} else if (concepts.includes('how-it-works')) {
|
||||
icon = '🔵';
|
||||
} else if (concepts.includes('what-changed')) {
|
||||
icon = '🟢';
|
||||
}
|
||||
|
||||
const time = formatTime(obs.created_at);
|
||||
const title = obs.title || 'Untitled';
|
||||
const tokens = estimateTokens(obs.narrative);
|
||||
|
||||
const showTime = time !== lastTime;
|
||||
const timeDisplay = showTime ? time : '';
|
||||
lastTime = time;
|
||||
|
||||
if (useColors) {
|
||||
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
|
||||
const tokensPart = tokens > 0 ? `${colors.dim}(~${tokens}t)${colors.reset}` : '';
|
||||
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${tokensPart}`);
|
||||
} else {
|
||||
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${tokens} |`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close final table if open
|
||||
if (tableOpen) {
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Add full summary details for most recent session
|
||||
const mostRecentSummary = recentSummaries[0];
|
||||
if (mostRecentSummary && (mostRecentSummary.completed || mostRecentSummary.next_steps)) {
|
||||
if (mostRecentSummary.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.green}Completed:${colors.reset} ${mostRecentSummary.completed}`);
|
||||
} else {
|
||||
output.push(`**Completed**: ${mostRecentSummary.completed}`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
|
||||
if (mostRecentSummary.next_steps) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.magenta}Next Steps:${colors.reset} ${mostRecentSummary.next_steps}`);
|
||||
} else {
|
||||
output.push(`**Next Steps**: ${mostRecentSummary.next_steps}`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Footer with MCP search instructions
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Use claude-mem MCP search to access records with the given ID${colors.reset}`);
|
||||
} else {
|
||||
output.push(`*Use claude-mem MCP search to access records with the given ID*`);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
return output.join('\n').trimEnd();
|
||||
}
|
||||
|
||||
// Entry Point - handle stdin/stdout
|
||||
const useIndexView = process.argv.includes('--index');
|
||||
const forceColors = process.argv.includes('--colors'); // Add this line
|
||||
|
||||
if (stdin.isTTY || forceColors) { // Modify this line to include forceColors
|
||||
// Running manually from terminal - print formatted output with colors
|
||||
const contextOutput = contextHook(undefined, true, useIndexView);
|
||||
console.log(contextOutput);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Running from hook - wrap in hookSpecificOutput JSON format
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
const contextOutput = contextHook(parsed, false, useIndexView);
|
||||
const result = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: contextOutput
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(result));
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
import path from 'path';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
transcript_path?: string;
|
||||
cwd?: string;
|
||||
hook_event_name?: string;
|
||||
source?: "startup" | "resume" | "clear" | "compact";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// ANSI color codes for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
gray: '\x1b[90m',
|
||||
};
|
||||
|
||||
/**
|
||||
* Context Hook - SessionStart
|
||||
* Shows user what happened in recent sessions
|
||||
*/
|
||||
export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string {
|
||||
ensureWorkerRunning();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Get the most recent summaries, then display them chronologically (oldest to newest, like a chat)
|
||||
const summaries = db.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(project) as Array<{
|
||||
sdk_session_id: string;
|
||||
request: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
|
||||
if (summaries.length === 0) {
|
||||
if (useColors) {
|
||||
return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous summaries found for this project yet.${colors.reset}\n`;
|
||||
}
|
||||
return `# [${project}] recent context\n\nNo previous summaries found for this project yet.`;
|
||||
}
|
||||
|
||||
const output: string[] = [];
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`# [${project}] recent context`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
let isFirstSummary = true;
|
||||
|
||||
for (let i = 0; i < summaries.length; i++) {
|
||||
const summary = summaries[i];
|
||||
|
||||
// Determine verbosity tier based on position
|
||||
// Most recent summary is at the end (highest index) since we display chronologically
|
||||
const positionFromEnd = summaries.length - 1 - i;
|
||||
const isTier1 = positionFromEnd === 0; // Most recent (full verbosity)
|
||||
const isTier2 = positionFromEnd >= 1 && positionFromEnd <= 3; // Middle 3 (request + what was done)
|
||||
const isTier3 = positionFromEnd > 3; // Oldest 6 (request only)
|
||||
|
||||
// Add separator between summaries (but not before the first one)
|
||||
if (!isFirstSummary) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push('---');
|
||||
output.push('');
|
||||
}
|
||||
} else {
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
isFirstSummary = false;
|
||||
|
||||
// TIER 3: Minimal (just Request + Date)
|
||||
if (isTier3) {
|
||||
if (summary.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
output.push('');
|
||||
}
|
||||
continue; // Skip the rest for Tier 3
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Request
|
||||
if (summary.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Show Learned
|
||||
if (isTier1 && summary.learned) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Learned:** ${summary.learned}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Completed
|
||||
if (summary.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Completed:** ${summary.completed}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Show Next Steps
|
||||
if (isTier1 && summary.next_steps) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Next Steps:** ${summary.next_steps}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Get and show files
|
||||
if (isTier1) {
|
||||
const observations = db.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(summary.sdk_session_id) as Array<{
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
}>;
|
||||
|
||||
const filesReadSet = new Set<string>();
|
||||
const filesModifiedSet = new Set<string>();
|
||||
|
||||
// Helper function to convert absolute paths to relative paths
|
||||
const toRelativePath = (filePath: string): string => {
|
||||
try {
|
||||
// Only convert if it's an absolute path
|
||||
if (path.isAbsolute(filePath)) {
|
||||
return path.relative(cwd, filePath);
|
||||
}
|
||||
return filePath;
|
||||
} catch {
|
||||
return filePath;
|
||||
}
|
||||
};
|
||||
|
||||
for (const obs of observations) {
|
||||
if (obs.files_read) {
|
||||
try {
|
||||
const files = JSON.parse(obs.files_read);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesReadSet.add(toRelativePath(f)));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
if (obs.files_modified) {
|
||||
try {
|
||||
const files = JSON.parse(obs.files_modified);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesModifiedSet.add(toRelativePath(f)));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove files from filesReadSet if they're already in filesModifiedSet (avoid redundancy)
|
||||
filesModifiedSet.forEach(file => filesReadSet.delete(file));
|
||||
|
||||
if (filesReadSet.size > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Read: ${Array.from(filesReadSet).join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Read:** ${Array.from(filesReadSet).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesModifiedSet.size > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Modified: ${Array.from(filesModifiedSet).join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Modified:** ${Array.from(filesModifiedSet).join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Date
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
}
|
||||
|
||||
if (!useColors) {
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
* Consolidated entry point + logic
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { stdin } from 'process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
prompt: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* New Hook Main Logic
|
||||
*/
|
||||
async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('newHook requires input');
|
||||
}
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
const project = path.basename(cwd);
|
||||
|
||||
// Ensure worker is running first
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Save session_id for indexing
|
||||
const sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
|
||||
// Save raw user prompt for full-text search
|
||||
db.saveUserPrompt(session_id, promptNumber, prompt);
|
||||
|
||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
|
||||
db.close();
|
||||
|
||||
// Get fixed port
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
}
|
||||
|
||||
// Entry Point
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input ? JSON.parse(input) : undefined;
|
||||
await newHook(parsed);
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import path from 'path';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
prompt: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
* Initializes SDK memory session via HTTP POST to worker service
|
||||
*/
|
||||
export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('newHook requires input');
|
||||
}
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
const project = path.basename(cwd);
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Just save session_id for indexing - no validation, no state management
|
||||
const sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
|
||||
// Save raw user prompt for full-text search
|
||||
db.saveUserPrompt(session_id, promptNumber, prompt);
|
||||
|
||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
|
||||
// Get fixed port
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Save Hook - PostToolUse
|
||||
* Consolidated entry point + logic
|
||||
*/
|
||||
|
||||
import { stdin } from 'process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -18,10 +24,9 @@ const SKIP_TOOLS = new Set([
|
||||
]);
|
||||
|
||||
/**
|
||||
* Save Hook - PostToolUse
|
||||
* Sends tool observations to worker via HTTP POST
|
||||
* Save Hook Main Logic
|
||||
*/
|
||||
export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('saveHook requires input');
|
||||
}
|
||||
@@ -33,7 +38,7 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
// Ensure worker is running first
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
@@ -41,14 +46,14 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get or create session - no validation, just use the session_id from hook
|
||||
const sessionDbId = db.createSDKSession(session_id, '', ''); // project and prompt not needed for observations
|
||||
// Get or create session
|
||||
const sessionDbId = db.createSDKSession(session_id, '', '');
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
db.close();
|
||||
|
||||
const toolStr = logger.formatTool(tool_name, tool_input);
|
||||
|
||||
// Use fixed worker port - no session.worker_port validation needed
|
||||
// Use fixed worker port
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
|
||||
@@ -80,3 +85,11 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
logger.debug('HOOK', 'Observation sent successfully', { sessionId: sessionDbId, toolName: tool_name });
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
}
|
||||
|
||||
// Entry Point
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input ? JSON.parse(input) : undefined;
|
||||
await saveHook(parsed);
|
||||
});
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Summary Hook - Stop
|
||||
* Consolidated entry point + logic
|
||||
*/
|
||||
|
||||
import { stdin } from 'process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -10,17 +16,16 @@ export interface StopInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary Hook - Stop
|
||||
* Sends SUMMARIZE message to worker via HTTP POST (not finalize - keeps SDK agent running)
|
||||
* Summary Hook Main Logic
|
||||
*/
|
||||
export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('summaryHook requires input');
|
||||
}
|
||||
|
||||
const { session_id } = input;
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
// Ensure worker is running first
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
@@ -28,12 +33,12 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get or create session - no validation, just use the session_id from hook
|
||||
// Get or create session
|
||||
const sessionDbId = db.createSDKSession(session_id, '', '');
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
db.close();
|
||||
|
||||
// Use fixed worker port - no session.worker_port validation needed
|
||||
// Use fixed worker port
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
@@ -61,3 +66,11 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: sessionDbId });
|
||||
console.log(createHookResponse('Stop', true));
|
||||
}
|
||||
|
||||
// Entry Point
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input ? JSON.parse(input) : undefined;
|
||||
await summaryHook(parsed);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* User Message Hook - SessionStart
|
||||
* Displays context information to the user via stderr
|
||||
*
|
||||
* This hook runs in parallel with context-hook to show users what context
|
||||
* has been loaded into their session. Uses stderr as the communication channel
|
||||
* since it's currently the only way to display messages in Claude Code UI.
|
||||
*/
|
||||
import { execSync } from "child_process";
|
||||
import { join } from "path";
|
||||
import { homedir } from "os";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
// Check if node_modules exists - if not, this is first run
|
||||
const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||
const nodeModulesPath = join(pluginDir, 'node_modules');
|
||||
|
||||
if (!existsSync(nodeModulesPath)) {
|
||||
// First-time installation - dependencies not yet installed
|
||||
console.error(`
|
||||
---
|
||||
🎉 Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
|
||||
user messages in Claude Code UI until a better method is provided.
|
||||
---
|
||||
|
||||
⚠️ Claude-Mem: First-Time Setup
|
||||
|
||||
Dependencies have been installed in the background. This only happens once.
|
||||
|
||||
💡 TIPS:
|
||||
• Memories will start generating while you work
|
||||
• Use /init to write or update your CLAUDE.md for better project context
|
||||
• Try /clear after one session to see what context looks like
|
||||
|
||||
Thank you for installing Claude-Mem!
|
||||
|
||||
This message was not added to your startup context, so you can continue working as normal.
|
||||
`);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
try {
|
||||
// Cross-platform path to context-hook.js in the installed plugin
|
||||
const contextHookPath = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', 'scripts', 'context-hook.js');
|
||||
const output = execSync(`node "${contextHookPath}" --colors`, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
console.error(
|
||||
"\n\n📝 Claude-Mem Context Loaded\n" +
|
||||
" ℹ️ Note: This appears as stderr but is informational only\n\n" +
|
||||
output
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to load context display: ${error}`);
|
||||
}
|
||||
|
||||
process.exit(3);
|
||||
@@ -1,559 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* SDK Worker Process
|
||||
* Background server that processes tool observations via Unix socket
|
||||
*/
|
||||
|
||||
// Bun-specific ImportMeta extension
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
main: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
import net from 'net';
|
||||
import { unlinkSync, existsSync } from 'fs';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { SDKUserMessage, SDKSystemMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { getWorkerSocketPath } from '../shared/paths.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt } from './prompts.js';
|
||||
import { parseObservations, parseSummary } from './parser.js';
|
||||
import type { SDKSession } from './prompts.js';
|
||||
|
||||
const MODEL = 'claude-sonnet-4-5';
|
||||
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
|
||||
|
||||
interface ObservationMessage {
|
||||
type: 'observation';
|
||||
tool_name: string;
|
||||
tool_input: string;
|
||||
tool_output: string;
|
||||
}
|
||||
|
||||
interface FinalizeMessage {
|
||||
type: 'finalize';
|
||||
}
|
||||
|
||||
type WorkerMessage = ObservationMessage | FinalizeMessage;
|
||||
|
||||
/**
|
||||
* Main worker process entry point
|
||||
*/
|
||||
export async function main() {
|
||||
console.error('[SDK Worker DEBUG] main() called');
|
||||
const sessionDbId = parseInt(process.argv[2], 10);
|
||||
console.error(`[SDK Worker DEBUG] Session DB ID: ${sessionDbId}`);
|
||||
|
||||
if (!sessionDbId) {
|
||||
console.error('[SDK Worker] Missing session ID argument');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const worker = new SDKWorker(sessionDbId);
|
||||
console.error('[SDK Worker DEBUG] SDKWorker instance created');
|
||||
await worker.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK Worker - Unix socket server that processes observations
|
||||
*/
|
||||
class SDKWorker {
|
||||
private sessionDbId: number;
|
||||
private db: SessionStore;
|
||||
private socketPath: string;
|
||||
private server: net.Server | null = null;
|
||||
private sdkSessionId: string | null = null;
|
||||
private project: string = '';
|
||||
private userPrompt: string = '';
|
||||
private abortController: AbortController;
|
||||
private isFinalized = false;
|
||||
private pendingMessages: WorkerMessage[] = [];
|
||||
|
||||
constructor(sessionDbId: number) {
|
||||
this.sessionDbId = sessionDbId;
|
||||
this.db = new SessionStore();
|
||||
this.abortController = new AbortController();
|
||||
this.socketPath = getWorkerSocketPath(sessionDbId);
|
||||
console.error('[claude-mem worker] Worker instance created', {
|
||||
sessionDbId,
|
||||
socketPath: this.socketPath
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main run loop
|
||||
*/
|
||||
async run(): Promise<void> {
|
||||
console.error('[claude-mem worker] Worker run() started', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
socketPath: this.socketPath
|
||||
});
|
||||
|
||||
try {
|
||||
// Load session info
|
||||
const session = await this.loadSession();
|
||||
if (!session) {
|
||||
console.error('[claude-mem worker] Session not found in database', {
|
||||
sessionDbId: this.sessionDbId
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.error('[claude-mem worker] Session loaded successfully', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
project: session.project,
|
||||
sdkSessionId: session.sdk_session_id,
|
||||
userPromptLength: session.user_prompt?.length || 0
|
||||
});
|
||||
|
||||
this.project = session.project;
|
||||
this.userPrompt = session.user_prompt;
|
||||
|
||||
// Start Unix socket server
|
||||
await this.startSocketServer();
|
||||
console.error('[claude-mem worker] Socket server started successfully', {
|
||||
socketPath: this.socketPath,
|
||||
sessionDbId: this.sessionDbId
|
||||
});
|
||||
|
||||
// Run SDK agent with streaming input
|
||||
console.error('[claude-mem worker] Starting SDK agent', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
model: MODEL
|
||||
});
|
||||
await this.runSDKAgent();
|
||||
|
||||
// Mark session as completed
|
||||
console.error('[claude-mem worker] SDK agent completed, marking session as completed', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId
|
||||
});
|
||||
this.db.markSessionCompleted(this.sessionDbId);
|
||||
this.db.close();
|
||||
this.cleanup();
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[claude-mem worker] Fatal error in run()', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
this.db.markSessionFailed(this.sessionDbId);
|
||||
this.db.close();
|
||||
this.cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Unix socket server to receive messages from hooks
|
||||
*/
|
||||
private async startSocketServer(): Promise<void> {
|
||||
console.error(`[SDK Worker DEBUG] Starting socket server...`);
|
||||
console.error(`[SDK Worker DEBUG] Socket path: ${this.socketPath}`);
|
||||
|
||||
// Clean up old socket if it exists
|
||||
if (existsSync(this.socketPath)) {
|
||||
console.error(`[SDK Worker DEBUG] Removing existing socket`);
|
||||
unlinkSync(this.socketPath);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.error(`[SDK Worker DEBUG] Creating net server...`);
|
||||
this.server = net.createServer((socket) => {
|
||||
console.error('[claude-mem worker] Socket connection received', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
socketPath: this.socketPath
|
||||
});
|
||||
let buffer = '';
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
console.error('[claude-mem worker] Data received on socket', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
chunkSize: chunk.length
|
||||
});
|
||||
buffer += chunk.toString();
|
||||
|
||||
// Try to parse complete JSON messages (separated by newlines)
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const message: WorkerMessage = JSON.parse(line);
|
||||
console.error('[claude-mem worker] Message received from socket', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
messageType: message.type,
|
||||
rawMessage: line.substring(0, 500) // Truncate to avoid massive logs
|
||||
});
|
||||
this.handleMessage(message);
|
||||
} catch (err) {
|
||||
console.error('[claude-mem worker] Invalid message - failed to parse JSON', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
rawLine: line.substring(0, 200)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('[claude-mem worker] Socket connection error', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
error: err.message,
|
||||
stack: err.stack
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error('[claude-mem worker] Socket already in use', {
|
||||
socketPath: this.socketPath,
|
||||
sessionDbId: this.sessionDbId
|
||||
});
|
||||
} else {
|
||||
console.error('[claude-mem worker] Server error', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
stack: err.stack
|
||||
});
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.server.listen(this.socketPath, () => {
|
||||
console.error(`[SDK Worker DEBUG] listen() callback fired`);
|
||||
console.error(`[SDK Worker DEBUG] Checking if socket exists: ${existsSync(this.socketPath)}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming message from hook
|
||||
*/
|
||||
private handleMessage(message: WorkerMessage): void {
|
||||
console.error('[claude-mem worker] Processing message in handleMessage()', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
messageType: message.type,
|
||||
pendingMessagesCount: this.pendingMessages.length
|
||||
});
|
||||
|
||||
this.pendingMessages.push(message);
|
||||
|
||||
if (message.type === 'finalize') {
|
||||
console.error('[claude-mem worker] FINALIZE message detected - queued for processing', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
pendingMessagesCount: this.pendingMessages.length
|
||||
});
|
||||
// DON'T set isFinalized here - let the generator set it after yielding finalize prompt
|
||||
} else if (message.type === 'observation') {
|
||||
console.error('[claude-mem worker] Observation message queued', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
toolName: message.tool_name,
|
||||
inputLength: message.tool_input?.length || 0,
|
||||
outputLength: message.tool_output?.length || 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load session from database
|
||||
*/
|
||||
private async loadSession(): Promise<SDKSession | null> {
|
||||
const db = this.db as any;
|
||||
const query = db.db.query(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
const session = query.get(this.sessionDbId);
|
||||
return session as SDKSession | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run SDK agent with streaming input mode
|
||||
*/
|
||||
private async runSDKAgent(): Promise<void> {
|
||||
// Find Claude Code executable
|
||||
const claudePath = process.env.CLAUDE_CODE_PATH || '/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude';
|
||||
console.error(`[SDK Worker DEBUG] About to call query with claudePath: ${claudePath}`);
|
||||
|
||||
const queryResult = query({
|
||||
prompt: this.createMessageGenerator(),
|
||||
options: {
|
||||
model: MODEL,
|
||||
disallowedTools: DISALLOWED_TOOLS,
|
||||
abortController: this.abortController,
|
||||
pathToClaudeCodeExecutable: claudePath
|
||||
}
|
||||
});
|
||||
|
||||
// Iterate over SDK messages
|
||||
for await (const message of queryResult) {
|
||||
// Handle system init message to capture session ID
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
const systemMsg = message as SDKSystemMessage;
|
||||
if (systemMsg.session_id) {
|
||||
console.error('[claude-mem worker] SDK session initialized', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: systemMsg.session_id
|
||||
});
|
||||
this.sdkSessionId = systemMsg.session_id;
|
||||
this.db.updateSDKSessionId(this.sessionDbId, systemMsg.session_id);
|
||||
}
|
||||
}
|
||||
// Handle assistant messages
|
||||
else if (message.type === 'assistant') {
|
||||
const content = message.message.content;
|
||||
// Extract text content from message
|
||||
const textContent = Array.isArray(content)
|
||||
? content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('\n')
|
||||
: typeof content === 'string' ? content : '';
|
||||
|
||||
console.error('[claude-mem worker] SDK agent response received', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
contentLength: textContent.length,
|
||||
contentPreview: textContent.substring(0, 200)
|
||||
});
|
||||
// Parse and store observations from agent response
|
||||
this.handleAgentMessage(textContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async message generator for SDK streaming input
|
||||
* Now pulls from socket messages instead of polling database
|
||||
*/
|
||||
private async* createMessageGenerator(): AsyncIterable<SDKUserMessage> {
|
||||
// Yield initial prompt
|
||||
const claudeSessionId = `session-${this.sessionDbId}`;
|
||||
const initPrompt = buildInitPrompt(this.project, claudeSessionId, this.userPrompt);
|
||||
console.error('[claude-mem worker] Yielding initial prompt to SDK agent', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
claudeSessionId,
|
||||
project: this.project,
|
||||
promptLength: initPrompt.length
|
||||
});
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: this.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: initPrompt
|
||||
}
|
||||
};
|
||||
|
||||
// Process messages as they arrive via socket
|
||||
while (!this.isFinalized) {
|
||||
// Wait for messages to arrive
|
||||
if (this.pendingMessages.length === 0) {
|
||||
await this.sleep(100); // Short sleep, just to yield control
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process all pending messages
|
||||
while (this.pendingMessages.length > 0) {
|
||||
const message = this.pendingMessages.shift()!;
|
||||
|
||||
if (message.type === 'finalize') {
|
||||
console.error('[claude-mem worker] Processing FINALIZE message in generator', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId
|
||||
});
|
||||
this.isFinalized = true;
|
||||
const session = await this.loadSession();
|
||||
if (session) {
|
||||
const finalizePrompt = buildSummaryPrompt(session);
|
||||
console.error('[claude-mem worker] Yielding finalize prompt to SDK agent', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
promptLength: finalizePrompt.length,
|
||||
promptPreview: finalizePrompt.substring(0, 300)
|
||||
});
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: this.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: finalizePrompt
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.error('[claude-mem worker] Failed to load session for finalize prompt', {
|
||||
sessionDbId: this.sessionDbId
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.type === 'observation') {
|
||||
// Build observation prompt
|
||||
const observationPrompt = buildObservationPrompt({
|
||||
id: 0, // Not needed for prompt generation
|
||||
tool_name: message.tool_name,
|
||||
tool_input: message.tool_input,
|
||||
tool_output: message.tool_output,
|
||||
created_at_epoch: Date.now()
|
||||
});
|
||||
console.error('[claude-mem worker] Yielding observation prompt to SDK agent', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
toolName: message.tool_name,
|
||||
promptLength: observationPrompt.length
|
||||
});
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: this.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: observationPrompt
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agent message and parse observations/summaries
|
||||
*/
|
||||
private handleAgentMessage(content: string): void {
|
||||
console.error('[claude-mem worker] Parsing agent message for observations and summary', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
contentLength: content.length
|
||||
});
|
||||
|
||||
// Parse observations
|
||||
const observations = parseObservations(content);
|
||||
console.error('[claude-mem worker] Observations parsed from response', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
observationCount: observations.length
|
||||
});
|
||||
|
||||
for (const obs of observations) {
|
||||
if (this.sdkSessionId) {
|
||||
console.error('[claude-mem worker] Storing observation in database', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
project: this.project,
|
||||
observationType: obs.type,
|
||||
observationTextLength: obs.text?.length || 0
|
||||
});
|
||||
this.db.storeObservation(this.sdkSessionId, this.project, obs.type, obs.text);
|
||||
} else {
|
||||
console.error('[claude-mem worker] Cannot store observation - no SDK session ID', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
observationType: obs.type
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parse summary (if present)
|
||||
console.error('[claude-mem worker] Attempting to parse summary from response', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId
|
||||
});
|
||||
|
||||
const summary = parseSummary(content);
|
||||
if (summary && this.sdkSessionId) {
|
||||
console.error('[claude-mem worker] Summary parsed successfully', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
project: this.project,
|
||||
hasRequest: !!summary.request,
|
||||
hasInvestigated: !!summary.investigated,
|
||||
hasLearned: !!summary.learned,
|
||||
hasCompleted: !!summary.completed,
|
||||
filesReadCount: summary.files_read?.length || 0,
|
||||
filesEditedCount: summary.files_edited?.length || 0
|
||||
});
|
||||
|
||||
// Convert file arrays to JSON strings
|
||||
const summaryWithArrays = {
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
completed: summary.completed,
|
||||
next_steps: summary.next_steps,
|
||||
files_read: JSON.stringify(summary.files_read),
|
||||
files_edited: JSON.stringify(summary.files_edited),
|
||||
notes: summary.notes
|
||||
};
|
||||
|
||||
console.error('[claude-mem worker] Storing summary in database', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
project: this.project
|
||||
});
|
||||
|
||||
this.db.storeSummary(this.sdkSessionId, this.project, summaryWithArrays);
|
||||
|
||||
console.error('[claude-mem worker] Summary stored successfully in database', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId,
|
||||
project: this.project
|
||||
});
|
||||
} else if (summary && !this.sdkSessionId) {
|
||||
console.error('[claude-mem worker] Summary parsed but cannot store - no SDK session ID', {
|
||||
sessionDbId: this.sessionDbId
|
||||
});
|
||||
} else {
|
||||
console.error('[claude-mem worker] No summary found in response', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
sdkSessionId: this.sdkSessionId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup socket server and socket file
|
||||
*/
|
||||
private cleanup(): void {
|
||||
console.error('[claude-mem worker] Cleaning up worker resources', {
|
||||
sessionDbId: this.sessionDbId,
|
||||
socketPath: this.socketPath,
|
||||
hasServer: !!this.server,
|
||||
socketExists: existsSync(this.socketPath)
|
||||
});
|
||||
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
if (existsSync(this.socketPath)) {
|
||||
unlinkSync(this.socketPath);
|
||||
}
|
||||
|
||||
console.error('[claude-mem worker] Cleanup complete', {
|
||||
sessionDbId: this.sessionDbId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep helper
|
||||
*/
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (import.meta.main) {
|
||||
main().catch((error) => {
|
||||
console.error('[SDK Worker] Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -12,11 +12,41 @@ import { parseObservations, parseSummary } from '../sdk/parser.js';
|
||||
import type { SDKSession } from '../sdk/prompts.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureAllDataDirs } from '../shared/paths.js';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const MODEL = process.env.CLAUDE_MEM_MODEL || 'claude-sonnet-4-5';
|
||||
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
/**
|
||||
* Find Claude Code executable path using which (Unix/Mac) or where (Windows)
|
||||
*/
|
||||
function findClaudePath(): string {
|
||||
try {
|
||||
// Try environment variable first
|
||||
if (process.env.CLAUDE_CODE_PATH) {
|
||||
return process.env.CLAUDE_CODE_PATH;
|
||||
}
|
||||
|
||||
// Use which on Unix/Mac, where on Windows
|
||||
const command = process.platform === 'win32' ? 'where claude' : 'which claude';
|
||||
const result = execSync(command, { encoding: 'utf8' }).trim();
|
||||
|
||||
// On Windows, 'where' returns multiple lines if there are multiple matches, take the first
|
||||
const path = result.split('\n')[0].trim();
|
||||
|
||||
if (!path) {
|
||||
throw new Error('Claude executable not found in PATH');
|
||||
}
|
||||
|
||||
logger.info('SYSTEM', `Found Claude executable: ${path}`);
|
||||
return path;
|
||||
} catch (error: any) {
|
||||
logger.failure('SYSTEM', 'Failed to find Claude executable', {}, error);
|
||||
throw new Error('Claude Code executable not found. Please ensure claude is in your PATH or set CLAUDE_CODE_PATH environment variable.');
|
||||
}
|
||||
}
|
||||
|
||||
interface ObservationMessage {
|
||||
type: 'observation';
|
||||
tool_name: string;
|
||||
@@ -345,7 +375,8 @@ class WorkerService {
|
||||
private async runSDKAgent(session: ActiveSession): Promise<void> {
|
||||
logger.info('SDK', 'Agent starting', { sessionId: session.sessionDbId });
|
||||
|
||||
const claudePath = process.env.CLAUDE_CODE_PATH || '/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude';
|
||||
const claudePath = findClaudePath();
|
||||
logger.info('SDK', `Using Claude executable: ${claudePath}`, { sessionId: session.sessionDbId });
|
||||
|
||||
try {
|
||||
const queryResult = query({
|
||||
|
||||
Reference in New Issue
Block a user