Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bec05b07ac | |||
| a85500aec8 | |||
| 96497e93d5 | |||
| c85dbaa508 | |||
| a537433eae | |||
| a8ae879398 | |||
| 77b733c7d1 | |||
| 84f2061d8f | |||
| 1391df4fe8 | |||
| 71b29af00a | |||
| 0768fafd83 | |||
| 5ce656037e | |||
| e27f8e4963 | |||
| af145cfaef | |||
| 15fe0cfe3c | |||
| c0ed9bbcfd | |||
| 8040c6d559 | |||
| 7187220b24 | |||
| ca52950b2a |
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.4.0",
|
"version": "7.4.5",
|
||||||
"source": "./plugin",
|
"source": "./plugin",
|
||||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ See [operations/workflow.md](operations/workflow.md) for detailed step-by-step p
|
|||||||
7. Commit and create git tag
|
7. Commit and create git tag
|
||||||
8. Push and create GitHub release
|
8. Push and create GitHub release
|
||||||
9. Generate CHANGELOG.md from releases and commit
|
9. Generate CHANGELOG.md from releases and commit
|
||||||
|
10. Post Discord notification
|
||||||
|
|
||||||
## Common Scenarios
|
## Common Scenarios
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ See [operations/scenarios.md](operations/scenarios.md) for examples:
|
|||||||
- Create git tag with format `vX.Y.Z`
|
- Create git tag with format `vX.Y.Z`
|
||||||
- Create GitHub release from the tag
|
- Create GitHub release from the tag
|
||||||
- Generate CHANGELOG.md from releases after creating release
|
- Generate CHANGELOG.md from releases after creating release
|
||||||
|
- Post Discord notification after release
|
||||||
- Ask user if version type is unclear
|
- Ask user if version type is unclear
|
||||||
|
|
||||||
**NEVER:**
|
**NEVER:**
|
||||||
@@ -74,6 +76,7 @@ Before considering the task complete:
|
|||||||
- [ ] Commit and tags pushed to remote
|
- [ ] Commit and tags pushed to remote
|
||||||
- [ ] GitHub release created from the tag
|
- [ ] GitHub release created from the tag
|
||||||
- [ ] CHANGELOG.md generated and committed
|
- [ ] CHANGELOG.md generated and committed
|
||||||
|
- [ ] Discord notification sent
|
||||||
|
|
||||||
## Reference Commands
|
## Reference Commands
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,17 @@ git push
|
|||||||
- No manual editing required
|
- No manual editing required
|
||||||
- Single source of truth: GitHub releases
|
- Single source of truth: GitHub releases
|
||||||
|
|
||||||
|
## Step 11: Discord Notification
|
||||||
|
|
||||||
|
Post release announcement to the Discord updates channel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send Discord notification with release details
|
||||||
|
npm run discord:notify vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
This fetches the release notes from GitHub and posts a formatted embed to the Discord updates channel configured in `.env`.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
After completing all steps, verify:
|
After completing all steps, verify:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
datasets/
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -4,6 +4,59 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [7.4.4] - 2025-12-21
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
* Code quality: comprehensive nonsense audit cleanup (20 issues) by @thedotmack in #400
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.4.3...v7.4.4
|
||||||
|
|
||||||
|
## [7.4.3] - 2025-12-20
|
||||||
|
|
||||||
|
Added Discord notification script for release announcements.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `scripts/discord-release-notify.js` - Posts formatted release notifications to Discord using webhook URL from `.env`
|
||||||
|
- `npm run discord:notify <version>` - New npm script to trigger Discord notifications
|
||||||
|
- Updated version-bump skill workflow to include Discord notification step
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Set `DISCORD_UPDATES_WEBHOOK` in your `.env` file to enable release notifications.
|
||||||
|
|
||||||
|
## [7.4.2] - 2025-12-20
|
||||||
|
|
||||||
|
Patch release v7.4.2
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- Refactored worker commands from npm scripts to claude-mem CLI
|
||||||
|
- Added path alias script
|
||||||
|
- Fixed Windows worker stop/restart reliability (#395)
|
||||||
|
- Simplified build commands section in CLAUDE.md
|
||||||
|
|
||||||
|
## [7.4.1] - 2025-12-19
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- **MCP Server**: Redirect logs to stderr to preserve JSON-RPC protocol (#396)
|
||||||
|
- MCP uses stdio transport where stdout is reserved for JSON-RPC messages
|
||||||
|
- Console.log was writing startup logs to stdout, causing Claude Desktop to parse log lines as JSON and fail
|
||||||
|
|
||||||
|
## [7.4.0] - 2025-12-18
|
||||||
|
|
||||||
|
## What's New
|
||||||
|
|
||||||
|
### MCP Tool Token Reduction
|
||||||
|
|
||||||
|
Optimized MCP tool definitions for reduced token consumption in Claude Code sessions through progressive parameter disclosure.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Streamlined MCP tool schemas with minimal inline definitions
|
||||||
|
- Added `get_schema()` tool for on-demand parameter documentation
|
||||||
|
- Enhanced worker API with operation-based instruction loading
|
||||||
|
|
||||||
|
This release improves session efficiency by reducing the token overhead of MCP tool definitions while maintaining full functionality through progressive disclosure.
|
||||||
|
|
||||||
## [7.3.9] - 2025-12-18
|
## [7.3.9] - 2025-12-18
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|||||||
@@ -33,12 +33,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
|
|||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build-and-sync # Build, sync to marketplace, restart worker (most common)
|
npm run build-and-sync # Build, sync to marketplace, restart worker
|
||||||
npm run build # Compile TypeScript only
|
|
||||||
npm run sync-marketplace # Copy to ~/.claude/plugins only
|
|
||||||
npm run worker:restart # Restart worker service only
|
|
||||||
npm run worker:status # Check worker status
|
|
||||||
npm run worker:logs # View worker logs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Viewer UI**: http://localhost:37777
|
**Viewer UI**: http://localhost:37777
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ If you're experiencing issues, describe the problem to Claude and the troublesho
|
|||||||
|
|
||||||
**Common Issues:**
|
**Common Issues:**
|
||||||
|
|
||||||
- Worker not starting → `npm run worker:restart`
|
- Worker not starting → `claude-mem restart`
|
||||||
- No context appearing → `npm run test:context`
|
- No context appearing → `npm run test:context`
|
||||||
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
|
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
|
||||||
- Search not working → Check FTS5 tables exist
|
- Search not working → Check FTS5 tables exist
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ pm2 logs claude-mem-worker # View logs
|
|||||||
```bash
|
```bash
|
||||||
npm run worker:start # Start worker
|
npm run worker:start # Start worker
|
||||||
npm run worker:stop # Stop worker
|
npm run worker:stop # Stop worker
|
||||||
npm run worker:restart # Restart worker
|
claude-mem restart # Restart worker
|
||||||
npm run worker:status # Check status
|
npm run worker:status # Check status
|
||||||
npm run worker:logs # View logs
|
npm run worker:logs # View logs
|
||||||
```
|
```
|
||||||
@@ -305,7 +305,7 @@ No migration logic runs on subsequent sessions.
|
|||||||
| `pm2 list` | `npm run worker:status` | Shows worker status |
|
| `pm2 list` | `npm run worker:status` | Shows worker status |
|
||||||
| `pm2 start <script>` | `npm run worker:start` | Start worker |
|
| `pm2 start <script>` | `npm run worker:start` | Start worker |
|
||||||
| `pm2 stop claude-mem-worker` | `npm run worker:stop` | Stop worker |
|
| `pm2 stop claude-mem-worker` | `npm run worker:stop` | Stop worker |
|
||||||
| `pm2 restart claude-mem-worker` | `npm run worker:restart` | Restart worker |
|
| `pm2 restart claude-mem-worker` | `claude-mem restart` | Restart worker |
|
||||||
| `pm2 delete claude-mem-worker` | `npm run worker:stop` | Remove worker |
|
| `pm2 delete claude-mem-worker` | `npm run worker:stop` | Remove worker |
|
||||||
| `pm2 logs claude-mem-worker` | `npm run worker:logs` | View logs |
|
| `pm2 logs claude-mem-worker` | `npm run worker:logs` | View logs |
|
||||||
| `pm2 describe claude-mem-worker` | `npm run worker:status` | Detailed status |
|
| `pm2 describe claude-mem-worker` | `npm run worker:status` | Detailed status |
|
||||||
@@ -451,7 +451,7 @@ pm2 save # Persist the deletion
|
|||||||
rm ~/.claude-mem/.pm2-migrated
|
rm ~/.claude-mem/.pm2-migrated
|
||||||
|
|
||||||
# Restart worker
|
# Restart worker
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scenario 2: Stale PID File (Process Dead)
|
### Scenario 2: Stale PID File (Process Dead)
|
||||||
@@ -483,7 +483,7 @@ lsof -i :37777
|
|||||||
kill -9 <PID>
|
kill -9 <PID>
|
||||||
|
|
||||||
# Restart worker
|
# Restart worker
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Common Error Messages
|
### Common Error Messages
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ If searches fail, check worker service:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run worker:status # Check status
|
npm run worker:status # Check status
|
||||||
npm run worker:restart # Restart worker
|
claude-mem restart # Restart worker
|
||||||
npm run worker:logs # View logs
|
npm run worker:logs # View logs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ npm run worker:start
|
|||||||
npm run worker:stop
|
npm run worker:stop
|
||||||
|
|
||||||
# Restart worker
|
# Restart worker
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ Edit `~/.claude-mem/settings.json`:
|
|||||||
|
|
||||||
Then restart the worker:
|
Then restart the worker:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Model
|
### Custom Model
|
||||||
@@ -331,7 +331,7 @@ Edit `~/.claude-mem/settings.json`:
|
|||||||
Then restart the worker:
|
Then restart the worker:
|
||||||
```bash
|
```bash
|
||||||
export CLAUDE_MEM_MODEL=opus
|
export CLAUDE_MEM_MODEL=opus
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Skip Tools
|
### Custom Skip Tools
|
||||||
@@ -388,7 +388,7 @@ Enable debug logging:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export DEBUG=claude-mem:*
|
export DEBUG=claude-mem:*
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ npm run worker:logs
|
|||||||
|
|
||||||
1. Restart worker after changes:
|
1. Restart worker after changes:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Verify environment variables:
|
2. Verify environment variables:
|
||||||
@@ -440,7 +440,7 @@ If port 37777 is already in use:
|
|||||||
|
|
||||||
2. Restart worker:
|
2. Restart worker:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Verify new port:
|
3. Verify new port:
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ npm run build
|
|||||||
1. Make changes to React components in `src/ui/viewer/`
|
1. Make changes to React components in `src/ui/viewer/`
|
||||||
2. Build: `npm run build`
|
2. Build: `npm run build`
|
||||||
3. Sync to installed plugin: `npm run sync-marketplace`
|
3. Sync to installed plugin: `npm run sync-marketplace`
|
||||||
4. Restart worker: `npm run worker:restart`
|
4. Restart worker: `claude-mem restart`
|
||||||
5. Refresh browser at http://localhost:37777
|
5. Refresh browser at http://localhost:37777
|
||||||
|
|
||||||
**Hot Reload**: Not currently supported. Full rebuild + restart required for changes.
|
**Hot Reload**: Not currently supported. Full rebuild + restart required for changes.
|
||||||
@@ -456,7 +456,7 @@ export async function createObservation(
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export DEBUG=claude-mem:*
|
export DEBUG=claude-mem:*
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ git checkout beta/endless-mode
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Restart the worker
|
# Restart the worker
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
**To return to stable:**
|
**To return to stable:**
|
||||||
@@ -103,7 +103,7 @@ npm run worker:restart
|
|||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
git checkout main
|
git checkout main
|
||||||
npm install
|
npm install
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ npm run worker:status
|
|||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
|
|
||||||
# Restart
|
# Restart
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
|
|
||||||
# Stop
|
# Stop
|
||||||
npm run worker:stop
|
npm run worker:stop
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ CLAUDE_MEM_PYTHON_VERSION=3.13 # Python version for chroma-mcp
|
|||||||
```bash
|
```bash
|
||||||
npm run build # Compile TypeScript (hooks + worker)
|
npm run build # Compile TypeScript (hooks + worker)
|
||||||
npm run sync-marketplace # Copy to ~/.claude/plugins
|
npm run sync-marketplace # Copy to ~/.claude/plugins
|
||||||
npm run worker:restart # Restart worker
|
claude-mem restart # Restart worker
|
||||||
npm run worker:logs # View worker logs
|
npm run worker:logs # View worker logs
|
||||||
npm run worker:status # Check worker status
|
npm run worker:status # Check worker status
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
|||||||
|
|
||||||
4. Restart worker service:
|
4. Restart worker service:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Check for port conflicts:
|
5. Check for port conflicts:
|
||||||
```bash
|
```bash
|
||||||
# If port 37777 is in use by another service
|
# If port 37777 is in use by another service
|
||||||
export CLAUDE_MEM_WORKER_PORT=38000
|
export CLAUDE_MEM_WORKER_PORT=38000
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Theme Toggle Not Persisting
|
### Theme Toggle Not Persisting
|
||||||
@@ -110,7 +110,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
|||||||
|
|
||||||
5. Restart worker and refresh browser:
|
5. Restart worker and refresh browser:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
### Chroma/Python Dependency Issues (v5.0.0+)
|
### Chroma/Python Dependency Issues (v5.0.0+)
|
||||||
@@ -225,7 +225,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
|||||||
3. Or use a different port:
|
3. Or use a different port:
|
||||||
```bash
|
```bash
|
||||||
export CLAUDE_MEM_WORKER_PORT=38000
|
export CLAUDE_MEM_WORKER_PORT=38000
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Verify new port:
|
4. Verify new port:
|
||||||
@@ -282,7 +282,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
|||||||
|
|
||||||
4. Restart worker:
|
4. Restart worker:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Hook Issues
|
## Hook Issues
|
||||||
@@ -644,7 +644,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
|||||||
|
|
||||||
2. Restart worker:
|
2. Restart worker:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Clean up old data (see "Database Too Large" above)
|
3. Clean up old data (see "Database Too Large" above)
|
||||||
@@ -721,7 +721,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export DEBUG=claude-mem:*
|
export DEBUG=claude-mem:*
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -781,7 +781,7 @@ SELECT created_at, tool_name FROM observations ORDER BY created_at DESC LIMIT 10
|
|||||||
|
|
||||||
**Cause**: Worker not running or port mismatch.
|
**Cause**: Worker not running or port mismatch.
|
||||||
|
|
||||||
**Solution**: Restart worker with `npm run worker:restart`.
|
**Solution**: Restart worker with `claude-mem restart`.
|
||||||
|
|
||||||
### "Database is locked"
|
### "Database is locked"
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ npm run worker:start
|
|||||||
npm run worker:stop
|
npm run worker:stop
|
||||||
|
|
||||||
# Restart worker service
|
# Restart worker service
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
|
|
||||||
# View worker logs
|
# View worker logs
|
||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ This design ensures that private content never reaches the database, search indi
|
|||||||
1. Verify correct syntax: `<private>content</private>`
|
1. Verify correct syntax: `<private>content</private>`
|
||||||
2. Check `~/.claude-mem/silent.log` for errors
|
2. Check `~/.claude-mem/silent.log` for errors
|
||||||
3. Ensure worker is running: `npm run worker:status`
|
3. Ensure worker is running: `npm run worker:status`
|
||||||
4. Restart worker: `npm run worker:restart`
|
4. Restart worker: `claude-mem restart`
|
||||||
|
|
||||||
### Partial Content Stored
|
### Partial Content Stored
|
||||||
|
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ If search isn't working, check the worker service:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run worker:status # Check worker status
|
npm run worker:status # Check worker status
|
||||||
npm run worker:restart # Restart if needed
|
claude-mem restart # Restart if needed
|
||||||
npm run worker:logs # View logs
|
npm run worker:logs # View logs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.4.0",
|
"version": "7.4.5",
|
||||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"worker:status": "bun plugin/scripts/worker-cli.js status",
|
"worker:status": "bun plugin/scripts/worker-cli.js status",
|
||||||
"worker:logs": "tail -n 50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
|
"worker:logs": "tail -n 50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
|
||||||
"changelog:generate": "node scripts/generate-changelog.js",
|
"changelog:generate": "node scripts/generate-changelog.js",
|
||||||
|
"discord:notify": "node scripts/discord-release-notify.js",
|
||||||
"usage:analyze": "node scripts/analyze-usage.js",
|
"usage:analyze": "node scripts/analyze-usage.js",
|
||||||
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)",
|
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)",
|
||||||
"translate-readme": "bun scripts/translate-readme/cli.ts -v -o docs/i18n README.md",
|
"translate-readme": "bun scripts/translate-readme/cli.ts -v -o docs/i18n README.md",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.4.0",
|
"version": "7.4.5",
|
||||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alex Newman"
|
"name": "Alex Newman"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem-plugin",
|
"name": "claude-mem-plugin",
|
||||||
"version": "7.4.0",
|
"version": "7.4.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12
-14
File diff suppressed because one or more lines are too long
@@ -253,6 +253,97 @@ function installUv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install the claude-mem CLI command to PATH
|
||||||
|
* Creates a wrapper script in ~/.local/bin (Unix) or %LOCALAPPDATA%\Programs\claude-mem (Windows)
|
||||||
|
*/
|
||||||
|
function installCLI() {
|
||||||
|
const CLI_NAME = 'claude-mem';
|
||||||
|
const WORKER_CLI = join(ROOT, 'plugin', 'scripts', 'worker-cli.js');
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Windows: Create .cmd file in LocalAppData
|
||||||
|
const cliDir = join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'Programs', 'claude-mem');
|
||||||
|
const cliPath = join(cliDir, `${CLI_NAME}.cmd`);
|
||||||
|
const markerPath = join(cliDir, '.cli-installed');
|
||||||
|
|
||||||
|
// Skip if already installed
|
||||||
|
if (existsSync(markerPath)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create directory if needed
|
||||||
|
if (!existsSync(cliDir)) {
|
||||||
|
execSync(`mkdir "${cliDir}"`, { stdio: 'ignore', shell: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Bun path for the wrapper
|
||||||
|
const bunPath = getBunPath() || 'bun';
|
||||||
|
|
||||||
|
// Create the wrapper script
|
||||||
|
const cmdContent = `@echo off
|
||||||
|
"${bunPath}" "${WORKER_CLI}" %*
|
||||||
|
`;
|
||||||
|
writeFileSync(cliPath, cmdContent);
|
||||||
|
writeFileSync(markerPath, new Date().toISOString());
|
||||||
|
|
||||||
|
console.error(`✅ CLI installed: ${cliPath}`);
|
||||||
|
console.error('');
|
||||||
|
console.error('📋 Add to PATH (run once in PowerShell as Admin):');
|
||||||
|
console.error(` [Environment]::SetEnvironmentVariable("Path", $env:Path + ";${cliDir}", "User")`);
|
||||||
|
console.error('');
|
||||||
|
console.error(' Then restart your terminal and use: claude-mem start|stop|restart|status');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`⚠️ Could not install CLI: ${error.message}`);
|
||||||
|
console.error(` You can still use: bun "${WORKER_CLI}" <command>`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unix: Create shell script in ~/.local/bin
|
||||||
|
const cliDir = join(homedir(), '.local', 'bin');
|
||||||
|
const cliPath = join(cliDir, CLI_NAME);
|
||||||
|
const markerPath = join(ROOT, '.cli-installed');
|
||||||
|
|
||||||
|
// Skip if already installed
|
||||||
|
if (existsSync(markerPath) && existsSync(cliPath)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create directory if needed
|
||||||
|
if (!existsSync(cliDir)) {
|
||||||
|
execSync(`mkdir -p "${cliDir}"`, { stdio: 'ignore', shell: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Bun path for the wrapper
|
||||||
|
const bunPath = getBunPath() || 'bun';
|
||||||
|
|
||||||
|
// Create the wrapper script
|
||||||
|
const shContent = `#!/usr/bin/env bash
|
||||||
|
# claude-mem CLI wrapper - manages the worker service
|
||||||
|
exec "${bunPath}" "${WORKER_CLI}" "$@"
|
||||||
|
`;
|
||||||
|
writeFileSync(cliPath, shContent, { mode: 0o755 });
|
||||||
|
writeFileSync(markerPath, new Date().toISOString());
|
||||||
|
|
||||||
|
console.error(`✅ CLI installed: ${cliPath}`);
|
||||||
|
|
||||||
|
// Check if ~/.local/bin is in PATH
|
||||||
|
const pathDirs = (process.env.PATH || '').split(':');
|
||||||
|
const localBinInPath = pathDirs.some(p => p === cliDir || p === '$HOME/.local/bin' || p.endsWith('/.local/bin'));
|
||||||
|
|
||||||
|
if (!localBinInPath) {
|
||||||
|
console.error('');
|
||||||
|
console.error('📋 Add to PATH (add to ~/.bashrc or ~/.zshrc):');
|
||||||
|
console.error(' export PATH="$HOME/.local/bin:$PATH"');
|
||||||
|
console.error('');
|
||||||
|
console.error(' Then restart your terminal and use: claude-mem start|stop|restart|status');
|
||||||
|
} else {
|
||||||
|
console.error(' Usage: claude-mem start|stop|restart|status');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`⚠️ Could not install CLI: ${error.message}`);
|
||||||
|
console.error(` You can still use: bun "${WORKER_CLI}" <command>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if dependencies need to be installed
|
* Check if dependencies need to be installed
|
||||||
*/
|
*/
|
||||||
@@ -351,6 +442,9 @@ try {
|
|||||||
installDeps();
|
installDeps();
|
||||||
console.error('✅ Dependencies installed');
|
console.error('✅ Dependencies installed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 4: Install CLI to PATH
|
||||||
|
installCLI();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ Installation failed:', e.message);
|
console.error('❌ Installation failed:', e.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+230
-185
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
"use strict";var u=Object.create;var w=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var y=(e,i,t,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of f(i))!k.call(e,s)&&s!==t&&w(e,s,{get:()=>i[s],enumerable:!(o=I(i,s))||o.enumerable});return e};var P=(e,i,t)=>(t=e!=null?u(g(e)):{},y(i||!e||!e.__esModule?w(t,"default",{value:e,enumerable:!0}):t,e));var c=require("child_process"),p=P(require("path"),1),h=process.platform==="win32",x=__dirname,l=p.default.join(x,"worker-service.cjs"),n=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function m(){r(`Spawning inner worker: ${l}`),n=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),n.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),n.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),n=null,!a&&e!==0&&(r("Inner crashed, respawning in 1 second..."),setTimeout(()=>m(),1e3))}),n.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!n||!n.pid){r("No inner process to kill");return}let e=n.pid;if(r(`Killing inner process tree (pid=${e})`),h)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{n.kill("SIGTERM");let i=new Promise(o=>{if(!n){o();return}n.on("exit",()=>o())}),t=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,t]),n&&!n.killed&&(r("Inner did not exit gracefully, force killing"),n.kill("SIGKILL"))}await S(e,5e3),n=null,r("Inner process terminated")}async function S(e,i){let t=Date.now();for(;Date.now()-t<i;)try{process.kill(e,0),await new Promise(o=>setTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");m();
|
"use strict";var m=Object.create;var w=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var f=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var g=(e,i,n,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of I(i))!x.call(e,s)&&s!==n&&w(e,s,{get:()=>i[s],enumerable:!(o=u(i,s))||o.enumerable});return e};var k=(e,i,n)=>(n=e!=null?m(f(e)):{},g(i||!e||!e.__esModule?w(n,"default",{value:e,enumerable:!0}):n,e));var c=require("child_process"),p=k(require("path"),1),y=process.platform==="win32",P=__dirname,l=p.default.join(P,"worker-service.cjs"),t=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function h(){r(`Spawning inner worker: ${l}`),t=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),t.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),t.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),t=null,a||(r("Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)"),process.exit(e??1))}),t.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!t||!t.pid){r("No inner process to kill");return}let e=t.pid;if(r(`Killing inner process tree (pid=${e})`),y)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{t.kill("SIGTERM");let i=new Promise(o=>{if(!t){o();return}t.on("exit",()=>o())}),n=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,n]),t&&!t.killed&&(r("Inner did not exit gracefully, force killing"),t.kill("SIGKILL"))}await S(e,5e3),t=null,r("Inner process terminated")}async function S(e,i){let n=Date.now();for(;Date.now()-n<i;)try{process.kill(e,0),await new Promise(o=>setTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");h();
|
||||||
|
|||||||
Binary file not shown.
@@ -288,7 +288,7 @@ npm run worker:status
|
|||||||
If the worker is stopped, restart it:
|
If the worker is stopped, restart it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ npm run worker:status
|
|||||||
```bash
|
```bash
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
||||||
npm install && \
|
npm install && \
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fix: Stale PID File
|
## Fix: Stale PID File
|
||||||
@@ -70,7 +70,7 @@ curl -s http://127.0.0.1:37777/health
|
|||||||
mkdir -p ~/.claude-mem && \
|
mkdir -p ~/.claude-mem && \
|
||||||
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json && \
|
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json && \
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
||||||
npm run worker:restart && \
|
claude-mem restart && \
|
||||||
sleep 2 && \
|
sleep 2 && \
|
||||||
curl -s http://127.0.0.1:37778/health
|
curl -s http://127.0.0.1:37778/health
|
||||||
```
|
```
|
||||||
@@ -86,7 +86,7 @@ curl -s http://127.0.0.1:37778/health
|
|||||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup && \
|
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup && \
|
||||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;" && \
|
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;" && \
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
**If integrity check fails, recreate database:**
|
**If integrity check fails, recreate database:**
|
||||||
@@ -94,7 +94,7 @@ npm run worker:restart
|
|||||||
# WARNING: This deletes all memory data
|
# WARNING: This deletes all memory data
|
||||||
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.old && \
|
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.old && \
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fix: Clean Reinstall
|
## Fix: Clean Reinstall
|
||||||
@@ -135,7 +135,7 @@ find ~/.claude-mem/logs/ -name "worker-*.log" -mtime +7 -delete
|
|||||||
|
|
||||||
# Restart worker for fresh log
|
# Restart worker for fresh log
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Logs auto-rotate daily, manual cleanup rarely needed.
|
**Note:** Logs auto-rotate daily, manual cleanup rarely needed.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Quick fixes for frequently encountered claude-mem problems.
|
|||||||
3. Restart worker and start new session:
|
3. Restart worker and start new session:
|
||||||
```bash
|
```bash
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create a test observation: `/skill version-bump` then cancel
|
4. Create a test observation: `/skill version-bump` then cancel
|
||||||
@@ -173,7 +173,7 @@ Quick fixes for frequently encountered claude-mem problems.
|
|||||||
4. If FTS5 out of sync, restart worker (triggers reindex):
|
4. If FTS5 out of sync, restart worker (triggers reindex):
|
||||||
```bash
|
```bash
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Issue: Port Conflicts
|
## Issue: Port Conflicts
|
||||||
@@ -194,7 +194,7 @@ Quick fixes for frequently encountered claude-mem problems.
|
|||||||
mkdir -p ~/.claude-mem
|
mkdir -p ~/.claude-mem
|
||||||
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
|
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Issue: Database Corrupted
|
## Issue: Database Corrupted
|
||||||
@@ -219,7 +219,7 @@ Quick fixes for frequently encountered claude-mem problems.
|
|||||||
```bash
|
```bash
|
||||||
rm ~/.claude-mem/claude-mem.db
|
rm ~/.claude-mem/claude-mem.db
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
# Worker will create new database
|
# Worker will create new database
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ If FTS5 counts don't match, triggers may have failed. Restart worker to rebuild:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
The worker will rebuild FTS5 indexes on startup if they're out of sync.
|
The worker will rebuild FTS5 indexes on startup if they're out of sync.
|
||||||
@@ -263,7 +263,7 @@ sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
|||||||
```bash
|
```bash
|
||||||
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.archive
|
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.archive
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database Recovery
|
## Database Recovery
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ npm run worker:status
|
|||||||
npm run worker:start
|
npm run worker:start
|
||||||
|
|
||||||
# Restart worker
|
# Restart worker
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
|
|
||||||
# Stop worker
|
# Stop worker
|
||||||
npm run worker:stop
|
npm run worker:stop
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ npm run worker:start
|
|||||||
```bash
|
```bash
|
||||||
# Restart worker (stops and starts)
|
# Restart worker (stops and starts)
|
||||||
cd ~/.claude/plugins/marketplaces/thedotmack/
|
cd ~/.claude/plugins/marketplaces/thedotmack/
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
|
|
||||||
# Or manually stop and start
|
# Or manually stop and start
|
||||||
npm run worker:stop
|
npm run worker:stop
|
||||||
@@ -219,7 +219,7 @@ npm run worker:start
|
|||||||
**Port conflict:**
|
**Port conflict:**
|
||||||
```bash
|
```bash
|
||||||
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
|
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
**Stale PID file:**
|
**Stale PID file:**
|
||||||
@@ -261,14 +261,14 @@ If fails, backup and recreate database.
|
|||||||
**Out of memory:**
|
**Out of memory:**
|
||||||
Check if database is too large or memory leak. Restart:
|
Check if database is too large or memory leak. Restart:
|
||||||
```bash
|
```bash
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
**Port conflict race condition:**
|
**Port conflict race condition:**
|
||||||
Another process grabbing port intermittently. Change port:
|
Another process grabbing port intermittently. Change port:
|
||||||
```bash
|
```bash
|
||||||
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
|
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Worker Management Commands
|
## Worker Management Commands
|
||||||
@@ -284,7 +284,7 @@ npm run worker:start
|
|||||||
npm run worker:stop
|
npm run worker:stop
|
||||||
|
|
||||||
# Restart worker
|
# Restart worker
|
||||||
npm run worker:restart
|
claude-mem restart
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
npm run worker:logs
|
npm run worker:logs
|
||||||
@@ -355,7 +355,7 @@ All should return appropriate responses (HTML for viewer, JSON for APIs).
|
|||||||
|---------|---------|----------------|
|
|---------|---------|----------------|
|
||||||
| Check if running | `npm run worker:status` | Shows PID and uptime |
|
| Check if running | `npm run worker:status` | Shows PID and uptime |
|
||||||
| Worker not running | `npm run worker:start` | Worker starts successfully |
|
| Worker not running | `npm run worker:start` | Worker starts successfully |
|
||||||
| Worker crashed | `npm run worker:restart` | Worker restarts |
|
| Worker crashed | `claude-mem restart` | Worker restarts |
|
||||||
| View recent errors | `grep -i error ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log \| tail -20` | Shows recent errors |
|
| View recent errors | `grep -i error ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log \| tail -20` | Shows recent errors |
|
||||||
| Port in use | `lsof -i :37777` | Shows process using port |
|
| Port in use | `lsof -i :37777` | Shows process using port |
|
||||||
| Stale PID | `rm ~/.claude-mem/worker.pid && npm run worker:start` | Removes stale PID and starts |
|
| Stale PID | `rm ~/.claude-mem/worker.pid && npm run worker:start` | Removes stale PID and starts |
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post release notification to Discord
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/discord-release-notify.js v7.4.2
|
||||||
|
* node scripts/discord-release-notify.js v7.4.2 "Custom release notes"
|
||||||
|
*
|
||||||
|
* Requires DISCORD_UPDATES_WEBHOOK in .env file
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { readFileSync, existsSync } from 'fs';
|
||||||
|
import { resolve, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = resolve(__dirname, '..');
|
||||||
|
|
||||||
|
function loadEnv() {
|
||||||
|
const envPath = resolve(projectRoot, '.env');
|
||||||
|
if (!existsSync(envPath)) {
|
||||||
|
console.error('❌ .env file not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envContent = readFileSync(envPath, 'utf-8');
|
||||||
|
const webhookMatch = envContent.match(/DISCORD_UPDATES_WEBHOOK=(.+)/);
|
||||||
|
|
||||||
|
if (!webhookMatch) {
|
||||||
|
console.error('❌ DISCORD_UPDATES_WEBHOOK not found in .env');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return webhookMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReleaseNotes(version) {
|
||||||
|
try {
|
||||||
|
const notes = execSync(`gh release view ${version} --json body --jq '.body'`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
cwd: projectRoot,
|
||||||
|
}).trim();
|
||||||
|
return notes;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanNotes(notes) {
|
||||||
|
// Remove Claude Code footer and clean up
|
||||||
|
return notes
|
||||||
|
.replace(/🤖 Generated with \[Claude Code\].*$/s, '')
|
||||||
|
.replace(/---\n*$/s, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(text, maxLength) {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.slice(0, maxLength - 3) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postToDiscord(webhookUrl, version, notes) {
|
||||||
|
const cleanedNotes = notes ? cleanNotes(notes) : 'No release notes available.';
|
||||||
|
const repoUrl = 'https://github.com/thedotmack/claude-mem';
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: `🚀 claude-mem ${version} released`,
|
||||||
|
url: `${repoUrl}/releases/tag/${version}`,
|
||||||
|
description: truncate(cleanedNotes, 2000),
|
||||||
|
color: 0x7c3aed, // Purple
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: '📦 Install',
|
||||||
|
value: 'Update via Claude Code plugin marketplace',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '📚 Docs',
|
||||||
|
value: '[docs.claude-mem.ai](https://docs.claude-mem.ai)',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
footer: {
|
||||||
|
text: 'claude-mem • Persistent memory for Claude Code',
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Discord API error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const version = process.argv[2];
|
||||||
|
const customNotes = process.argv[3];
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
console.error('Usage: node scripts/discord-release-notify.js <version> [notes]');
|
||||||
|
console.error('Example: node scripts/discord-release-notify.js v7.4.2');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📣 Posting release notification for ${version}...`);
|
||||||
|
|
||||||
|
const webhookUrl = loadEnv();
|
||||||
|
const notes = customNotes || getReleaseNotes(version);
|
||||||
|
|
||||||
|
if (!notes && !customNotes) {
|
||||||
|
console.warn('⚠️ Could not fetch release notes from GitHub, proceeding without them');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postToDiscord(webhookUrl, version, notes);
|
||||||
|
console.log('✅ Discord notification sent successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to send Discord notification:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-time script to extract tool handlers from mcp-server.ts into SearchManager.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
const projectRoot = join(__dirname, '..');
|
|
||||||
|
|
||||||
const mcpServerPath = join(projectRoot, 'src/servers/mcp-server.ts');
|
|
||||||
const outputPath = join(projectRoot, 'src/services/worker/SearchManager.ts');
|
|
||||||
|
|
||||||
console.log('Reading mcp-server.ts...');
|
|
||||||
const content = readFileSync(mcpServerPath, 'utf-8');
|
|
||||||
|
|
||||||
// Extract just the sections we need by finding line numbers
|
|
||||||
// This is more reliable than parsing
|
|
||||||
|
|
||||||
// Extract tool handler bodies by finding each "handler: async (args: any) => {"
|
|
||||||
// and extracting until the matching closing brace
|
|
||||||
|
|
||||||
const extractHandlerBody = (content, startPattern) => {
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const startIdx = lines.findIndex(line => line.includes(startPattern));
|
|
||||||
|
|
||||||
if (startIdx === -1) return null;
|
|
||||||
|
|
||||||
// Find the "handler: async (args: any) => {" line
|
|
||||||
let handlerIdx = -1;
|
|
||||||
for (let i = startIdx; i < Math.min(startIdx + 30, lines.length); i++) {
|
|
||||||
if (lines[i].includes('handler: async (args: any) => {')) {
|
|
||||||
handlerIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handlerIdx === -1) return null;
|
|
||||||
|
|
||||||
// Extract the body by counting braces
|
|
||||||
let braceCount = 0;
|
|
||||||
let bodyLines = [];
|
|
||||||
let started = false;
|
|
||||||
|
|
||||||
for (let i = handlerIdx; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
for (const char of line) {
|
|
||||||
if (char === '{') {
|
|
||||||
braceCount++;
|
|
||||||
started = true;
|
|
||||||
} else if (char === '}') {
|
|
||||||
braceCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (started) {
|
|
||||||
bodyLines.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (started && braceCount === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the first line (handler wrapper) and last line (closing brace)
|
|
||||||
if (bodyLines.length > 2) {
|
|
||||||
bodyLines = bodyLines.slice(1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bodyLines.join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tool name to search pattern mapping
|
|
||||||
const tools = {
|
|
||||||
'search': "name: 'search'",
|
|
||||||
'timeline': "name: 'timeline'",
|
|
||||||
'decisions': "name: 'decisions'",
|
|
||||||
'changes': "name: 'changes'",
|
|
||||||
'how_it_works': "name: 'how_it_works'",
|
|
||||||
'search_observations': "name: 'search_observations'",
|
|
||||||
'search_sessions': "name: 'search_sessions'",
|
|
||||||
'search_user_prompts': "name: 'search_user_prompts'",
|
|
||||||
'find_by_concept': "name: 'find_by_concept'",
|
|
||||||
'find_by_file': "name: 'find_by_file'",
|
|
||||||
'find_by_type': "name: 'find_by_type'",
|
|
||||||
'get_recent_context': "name: 'get_recent_context'",
|
|
||||||
'get_context_timeline': "name: 'get_context_timeline'",
|
|
||||||
'get_timeline_by_query': "name: 'get_timeline_by_query'"
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Extracting tool handlers...');
|
|
||||||
const handlers = {};
|
|
||||||
|
|
||||||
for (const [toolName, pattern] of Object.entries(tools)) {
|
|
||||||
console.log(` Extracting ${toolName}...`);
|
|
||||||
const body = extractHandlerBody(content, pattern);
|
|
||||||
if (body) {
|
|
||||||
handlers[toolName] = body;
|
|
||||||
console.log(` ✓ ${body.split('\n').length} lines`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✗ Not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nExtracted ${Object.keys(handlers).length}/${Object.keys(tools).length} handlers`);
|
|
||||||
|
|
||||||
// Now generate SearchManager.ts
|
|
||||||
console.log('\nGenerating SearchManager.ts...');
|
|
||||||
|
|
||||||
const methodBodies = Object.entries(handlers).map(([toolName, body]) => {
|
|
||||||
// Convert tool name to camelCase method name
|
|
||||||
const methodName = toolName.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
||||||
|
|
||||||
// Replace standalone function calls with class methods
|
|
||||||
let processedBody = body
|
|
||||||
.replace(/formatSearchTips\(\)/g, 'this.formatter.formatSearchTips()')
|
|
||||||
.replace(/formatObservationIndex\(/g, 'this.formatter.formatObservationIndex(')
|
|
||||||
.replace(/formatSessionIndex\(/g, 'this.formatter.formatSessionIndex(')
|
|
||||||
.replace(/formatUserPromptIndex\(/g, 'this.formatter.formatUserPromptIndex(')
|
|
||||||
.replace(/formatObservationResult\(/g, 'this.formatter.formatObservationResult(')
|
|
||||||
.replace(/formatSessionResult\(/g, 'this.formatter.formatSessionResult(')
|
|
||||||
.replace(/formatUserPromptResult\(/g, 'this.formatter.formatUserPromptResult(')
|
|
||||||
.replace(/filterTimelineByDepth\(/g, 'this.timeline.filterByDepth(')
|
|
||||||
.replace(/\bsearch\./g, 'this.sessionSearch.')
|
|
||||||
.replace(/\bstore\./g, 'this.sessionStore.')
|
|
||||||
.replace(/queryChroma\(/g, 'this.queryChroma(')
|
|
||||||
.replace(/normalizeParams\(/g, 'this.normalizeParams(')
|
|
||||||
.replace(/chromaClient/g, 'this.chromaSync');
|
|
||||||
|
|
||||||
return ` /**
|
|
||||||
* Tool handler: ${toolName}
|
|
||||||
*/
|
|
||||||
async ${methodName}(args: any): Promise<any> {
|
|
||||||
${processedBody}
|
|
||||||
}`;
|
|
||||||
}).join('\n\n');
|
|
||||||
|
|
||||||
const searchManagerContent = `/**
|
|
||||||
* SearchManager - Core search orchestration for claude-mem
|
|
||||||
* Extracted from mcp-server.ts to centralize business logic in Worker services
|
|
||||||
*
|
|
||||||
* This class contains all tool handler logic that was previously in the MCP server.
|
|
||||||
* The MCP server now acts as a thin HTTP wrapper that calls these methods via HTTP.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SessionSearch } from '../sqlite/SessionSearch.js';
|
|
||||||
import { SessionStore } from '../sqlite/SessionStore.js';
|
|
||||||
import { ChromaSync } from '../sync/ChromaSync.js';
|
|
||||||
import { FormattingService } from './FormattingService.js';
|
|
||||||
import { TimelineService, TimelineItem } from './TimelineService.js';
|
|
||||||
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
|
|
||||||
import { silentDebug } from '../../utils/silent-debug.js';
|
|
||||||
|
|
||||||
const COLLECTION_NAME = 'cm__claude-mem';
|
|
||||||
|
|
||||||
export class SearchManager {
|
|
||||||
constructor(
|
|
||||||
private sessionSearch: SessionSearch,
|
|
||||||
private sessionStore: SessionStore,
|
|
||||||
private chromaSync: ChromaSync,
|
|
||||||
private formatter: FormattingService,
|
|
||||||
private timeline: TimelineService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query Chroma vector database via ChromaSync
|
|
||||||
*/
|
|
||||||
private async queryChroma(
|
|
||||||
query: string,
|
|
||||||
limit: number,
|
|
||||||
whereFilter?: Record<string, any>
|
|
||||||
): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> {
|
|
||||||
return await this.chromaSync.queryChroma(query, limit, whereFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to normalize query parameters from URL-friendly format
|
|
||||||
* Converts comma-separated strings to arrays and flattens date params
|
|
||||||
*/
|
|
||||||
private normalizeParams(args: any): any {
|
|
||||||
const normalized: any = { ...args };
|
|
||||||
|
|
||||||
// Parse comma-separated concepts into array
|
|
||||||
if (normalized.concepts && typeof normalized.concepts === 'string') {
|
|
||||||
normalized.concepts = normalized.concepts.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse comma-separated files into array
|
|
||||||
if (normalized.files && typeof normalized.files === 'string') {
|
|
||||||
normalized.files = normalized.files.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse comma-separated obs_type into array
|
|
||||||
if (normalized.obs_type && typeof normalized.obs_type === 'string') {
|
|
||||||
normalized.obs_type = normalized.obs_type.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse comma-separated type (for filterSchema) into array
|
|
||||||
if (normalized.type && typeof normalized.type === 'string' && normalized.type.includes(',')) {
|
|
||||||
normalized.type = normalized.type.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten dateStart/dateEnd into dateRange object
|
|
||||||
if (normalized.dateStart || normalized.dateEnd) {
|
|
||||||
normalized.dateRange = {
|
|
||||||
start: normalized.dateStart,
|
|
||||||
end: normalized.dateEnd
|
|
||||||
};
|
|
||||||
delete normalized.dateStart;
|
|
||||||
delete normalized.dateEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
${methodBodies}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
writeFileSync(outputPath, searchManagerContent, 'utf-8');
|
|
||||||
|
|
||||||
console.log(`\n✅ SearchManager.ts generated at ${outputPath}`);
|
|
||||||
console.log(` Total methods: ${Object.keys(handlers).length + 2} (${Object.keys(handlers).length} tools + queryChroma + normalizeParams)`);
|
|
||||||
console.log(` File size: ${(searchManagerContent.length / 1024).toFixed(1)} KB`);
|
|
||||||
@@ -48,6 +48,8 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Worker might not be running - that's okay (non-critical)
|
// Worker might not be running - that's okay (non-critical)
|
||||||
|
// But we should still log it for visibility
|
||||||
|
console.error('[cleanup-hook] Failed to notify worker of session end:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('{"continue": true, "suppressOutput": true}');
|
console.log('{"continue": true, "suppressOutput": true}');
|
||||||
|
|||||||
@@ -1,72 +1,11 @@
|
|||||||
export type HookType = 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop';
|
|
||||||
|
|
||||||
export interface HookResponseOptions {
|
|
||||||
reason?: string;
|
|
||||||
context?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HookResponse {
|
|
||||||
continue?: boolean;
|
|
||||||
suppressOutput?: boolean;
|
|
||||||
stopReason?: string;
|
|
||||||
hookSpecificOutput?: {
|
|
||||||
hookEventName: 'SessionStart';
|
|
||||||
additionalContext: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHookResponse(
|
|
||||||
hookType: HookType,
|
|
||||||
success: boolean,
|
|
||||||
options: HookResponseOptions
|
|
||||||
): HookResponse {
|
|
||||||
if (hookType === 'SessionStart') {
|
|
||||||
if (success && options.context) {
|
|
||||||
return {
|
|
||||||
continue: true,
|
|
||||||
suppressOutput: true,
|
|
||||||
hookSpecificOutput: {
|
|
||||||
hookEventName: 'SessionStart',
|
|
||||||
additionalContext: options.context
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
continue: true,
|
|
||||||
suppressOutput: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') {
|
|
||||||
return {
|
|
||||||
continue: true,
|
|
||||||
suppressOutput: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hookType === 'Stop') {
|
|
||||||
return {
|
|
||||||
continue: true,
|
|
||||||
suppressOutput: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
continue: success,
|
|
||||||
suppressOutput: true,
|
|
||||||
...(options.reason && !success ? { stopReason: options.reason } : {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a standardized hook response using the HookTemplates system.
|
* Standard hook response for all hooks.
|
||||||
|
* Tells Claude Code to continue processing and suppress the hook's output.
|
||||||
|
*
|
||||||
|
* Note: SessionStart uses context-hook.ts which constructs its own response
|
||||||
|
* with hookSpecificOutput for context injection.
|
||||||
*/
|
*/
|
||||||
export function createHookResponse(
|
export const STANDARD_HOOK_RESPONSE = JSON.stringify({
|
||||||
hookType: HookType,
|
continue: true,
|
||||||
success: boolean,
|
suppressOutput: true
|
||||||
options: HookResponseOptions = {}
|
});
|
||||||
): string {
|
|
||||||
const response = buildHookResponse(hookType, success, options);
|
|
||||||
return JSON.stringify(response);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { stdin } from 'process';
|
import { stdin } from 'process';
|
||||||
import { createHookResponse } from './hook-response.js';
|
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
|
||||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||||
import { handleFetchError } from './shared/error-handler.js';
|
import { handleFetchError } from './shared/error-handler.js';
|
||||||
@@ -61,7 +61,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
|||||||
// Check if prompt was entirely private (worker performs privacy check)
|
// Check if prompt was entirely private (worker performs privacy check)
|
||||||
if (initResult.skipped && initResult.reason === 'private') {
|
if (initResult.skipped && initResult.reason === 'private') {
|
||||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber} (fully private - skipped)`);
|
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber} (fully private - skipped)`);
|
||||||
console.log(createHookResponse('UserPromptSubmit', true));
|
console.log(STANDARD_HOOK_RESPONSE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
|||||||
handleWorkerError(error);
|
handleWorkerError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(createHookResponse('UserPromptSubmit', true));
|
console.log(STANDARD_HOOK_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry Point
|
// Entry Point
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { stdin } from 'process';
|
import { stdin } from 'process';
|
||||||
import { createHookResponse } from './hook-response.js';
|
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||||
@@ -43,6 +43,11 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
|||||||
workerPort: port
|
workerPort: port
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validate required fields before sending to worker
|
||||||
|
if (!cwd) {
|
||||||
|
throw new Error(`Missing cwd in PostToolUse hook input for session ${session_id}, tool ${tool_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send to worker - worker handles privacy check and database operations
|
// Send to worker - worker handles privacy check and database operations
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
|
||||||
@@ -53,13 +58,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
|||||||
tool_name,
|
tool_name,
|
||||||
tool_input,
|
tool_input,
|
||||||
tool_response,
|
tool_response,
|
||||||
cwd: cwd || logger.happyPathError(
|
cwd
|
||||||
'HOOK',
|
|
||||||
'Missing cwd in PostToolUse hook input',
|
|
||||||
undefined,
|
|
||||||
{ session_id, tool_name },
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
|
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
|
||||||
});
|
});
|
||||||
@@ -80,7 +79,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
|||||||
handleWorkerError(error);
|
handleWorkerError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(createHookResponse('PostToolUse', true));
|
console.log(STANDARD_HOOK_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry Point
|
// Entry Point
|
||||||
|
|||||||
+18
-12
@@ -10,7 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { stdin } from 'process';
|
import { stdin } from 'process';
|
||||||
import { createHookResponse } from './hook-response.js';
|
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||||
@@ -39,16 +39,14 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
|||||||
|
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
|
// Validate required fields before processing
|
||||||
|
if (!input.transcript_path) {
|
||||||
|
throw new Error(`Missing transcript_path in Stop hook input for session ${session_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Extract last user AND assistant messages from transcript
|
// Extract last user AND assistant messages from transcript
|
||||||
const transcriptPath = input.transcript_path || logger.happyPathError(
|
const lastUserMessage = extractLastMessage(input.transcript_path, 'user');
|
||||||
'HOOK',
|
const lastAssistantMessage = extractLastMessage(input.transcript_path, 'assistant', true);
|
||||||
'Missing transcript_path in Stop hook input',
|
|
||||||
undefined,
|
|
||||||
{ session_id },
|
|
||||||
''
|
|
||||||
);
|
|
||||||
const lastUserMessage = extractLastMessage(transcriptPath, 'user');
|
|
||||||
const lastAssistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
|
|
||||||
|
|
||||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||||
workerPort: port,
|
workerPort: port,
|
||||||
@@ -56,6 +54,8 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
|||||||
hasLastAssistantMessage: !!lastAssistantMessage
|
hasLastAssistantMessage: !!lastAssistantMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let summaryError: Error | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send to worker - worker handles privacy check and database operations
|
// Send to worker - worker handles privacy check and database operations
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
|
||||||
@@ -81,9 +81,10 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
|||||||
|
|
||||||
logger.debug('HOOK', 'Summary request sent successfully');
|
logger.debug('HOOK', 'Summary request sent successfully');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
summaryError = error;
|
||||||
handleWorkerError(error);
|
handleWorkerError(error);
|
||||||
} finally {
|
} finally {
|
||||||
// Stop processing spinner
|
// Stop processing spinner (non-critical operation, errors are logged but don't block)
|
||||||
try {
|
try {
|
||||||
const spinnerResponse = await fetch(`http://127.0.0.1:${port}/api/processing`, {
|
const spinnerResponse = await fetch(`http://127.0.0.1:${port}/api/processing`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -99,7 +100,12 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(createHookResponse('Stop', true));
|
// Re-throw summary error after cleanup to ensure it's not masked by finally block
|
||||||
|
if (summaryError) {
|
||||||
|
throw summaryError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(STANDARD_HOOK_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry Point
|
// Entry Point
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
* Maintains MCP protocol handling and tool schemas
|
* Maintains MCP protocol handling and tool schemas
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// CRITICAL: Redirect console.log to stderr BEFORE any imports
|
||||||
|
// MCP uses stdio transport where stdout is reserved for JSON-RPC protocol messages.
|
||||||
|
// Any logs to stdout break the protocol (Claude Desktop parses "[2025..." as JSON array).
|
||||||
|
const _originalConsoleLog = console.log;
|
||||||
|
console.log = (...args: any[]) => console.error(...args);
|
||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import {
|
||||||
@@ -490,7 +496,7 @@ async function main() {
|
|||||||
if (!workerAvailable) {
|
if (!workerAvailable) {
|
||||||
logger.warn('SYSTEM', 'Worker not available', undefined, { workerUrl: WORKER_BASE_URL });
|
logger.warn('SYSTEM', 'Worker not available', undefined, { workerUrl: WORKER_BASE_URL });
|
||||||
logger.warn('SYSTEM', 'Tools will fail until Worker is started');
|
logger.warn('SYSTEM', 'Tools will fail until Worker is started');
|
||||||
logger.warn('SYSTEM', 'Start Worker with: npm run worker:restart');
|
logger.warn('SYSTEM', 'Start Worker with: claude-mem restart');
|
||||||
} else {
|
} else {
|
||||||
logger.info('SYSTEM', 'Worker available', undefined, { workerUrl: WORKER_BASE_URL });
|
logger.info('SYSTEM', 'Worker available', undefined, { workerUrl: WORKER_BASE_URL });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,12 @@ import { spawn, spawnSync } from 'child_process';
|
|||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { DATA_DIR } from '../../shared/paths.js';
|
import { DATA_DIR } from '../../shared/paths.js';
|
||||||
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
|
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
|
||||||
|
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
||||||
|
|
||||||
const PID_FILE = join(DATA_DIR, 'worker.pid');
|
const PID_FILE = join(DATA_DIR, 'worker.pid');
|
||||||
const LOG_DIR = join(DATA_DIR, 'logs');
|
const LOG_DIR = join(DATA_DIR, 'logs');
|
||||||
const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||||
|
|
||||||
// Timeout constants
|
|
||||||
const PROCESS_STOP_TIMEOUT_MS = 5000;
|
|
||||||
const HEALTH_CHECK_TIMEOUT_MS = 10000;
|
|
||||||
const HEALTH_CHECK_INTERVAL_MS = 200;
|
|
||||||
const HEALTH_CHECK_FETCH_TIMEOUT_MS = 1000;
|
|
||||||
const PROCESS_EXIT_CHECK_INTERVAL_MS = 100;
|
|
||||||
|
|
||||||
interface PidInfo {
|
interface PidInfo {
|
||||||
pid: number;
|
pid: number;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -99,8 +93,9 @@ export class ProcessManager {
|
|||||||
const escapedBunPath = this.escapePowerShellString(bunPath);
|
const escapedBunPath = this.escapePowerShellString(bunPath);
|
||||||
const escapedScript = this.escapePowerShellString(script);
|
const escapedScript = this.escapePowerShellString(script);
|
||||||
const escapedWorkDir = this.escapePowerShellString(MARKETPLACE_ROOT);
|
const escapedWorkDir = this.escapePowerShellString(MARKETPLACE_ROOT);
|
||||||
|
const escapedLogFile = this.escapePowerShellString(logFile);
|
||||||
const envVars = `$env:CLAUDE_MEM_WORKER_PORT='${port}'`;
|
const envVars = `$env:CLAUDE_MEM_WORKER_PORT='${port}'`;
|
||||||
const psCommand = `${envVars}; Start-Process -FilePath '${escapedBunPath}' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkDir}' -WindowStyle Hidden -PassThru | Select-Object -ExpandProperty Id`;
|
const psCommand = `${envVars}; Start-Process -FilePath '${escapedBunPath}' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkDir}' -WindowStyle Hidden -RedirectStandardOutput '${escapedLogFile}' -RedirectStandardError '${escapedLogFile}.err' -PassThru | Select-Object -ExpandProperty Id`;
|
||||||
|
|
||||||
const result = spawnSync('powershell', ['-Command', psCommand], {
|
const result = spawnSync('powershell', ['-Command', psCommand], {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
@@ -169,36 +164,67 @@ export class ProcessManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise<boolean> {
|
static async stop(timeout: number = 5000): Promise<boolean> {
|
||||||
const info = this.getPidInfo();
|
const info = this.getPidInfo();
|
||||||
if (!info) return true;
|
|
||||||
|
|
||||||
try {
|
if (process.platform === 'win32') {
|
||||||
if (process.platform === 'win32') {
|
// Windows: Try graceful HTTP shutdown first - this works regardless of PID file state
|
||||||
// On Windows, use taskkill /T /F to kill entire process tree
|
// because the worker shuts itself down from the inside (via wrapper IPC)
|
||||||
|
const port = info?.port ?? this.getPortFromSettings();
|
||||||
|
const httpShutdownSucceeded = await this.tryHttpShutdown(port);
|
||||||
|
|
||||||
|
if (httpShutdownSucceeded) {
|
||||||
|
// HTTP shutdown succeeded - worker confirmed down, safe to remove PID file
|
||||||
|
this.removePidFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP shutdown failed (worker not responding), fall back to taskkill
|
||||||
|
if (!info) {
|
||||||
|
// No PID file and HTTP failed - nothing more we can do
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { execSync } = await import('child_process');
|
||||||
|
try {
|
||||||
|
// Use taskkill /T /F to kill entire process tree
|
||||||
// This ensures the wrapper AND all its children (inner worker, MCP, ChromaSync) are killed
|
// This ensures the wrapper AND all its children (inner worker, MCP, ChromaSync) are killed
|
||||||
// which is necessary to properly release the socket and avoid zombie ports
|
// which is necessary to properly release the socket and avoid zombie ports
|
||||||
const { execSync } = await import('child_process');
|
execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
|
||||||
try {
|
} catch {
|
||||||
execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
|
// Process may already be dead
|
||||||
} catch {
|
}
|
||||||
// Process may already be dead
|
|
||||||
}
|
// Wait for process to actually exit before removing PID file
|
||||||
} else {
|
try {
|
||||||
// On Unix, use signals
|
await this.waitForExit(info.pid, timeout);
|
||||||
|
} catch {
|
||||||
|
// Timeout waiting - process may still be alive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only remove PID file if process is confirmed dead
|
||||||
|
if (!this.isProcessAlive(info.pid)) {
|
||||||
|
this.removePidFile();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Unix: Use signals (unchanged behavior)
|
||||||
|
if (!info) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
process.kill(info.pid, 'SIGTERM');
|
process.kill(info.pid, 'SIGTERM');
|
||||||
await this.waitForExit(info.pid, timeout);
|
await this.waitForExit(info.pid, timeout);
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
process.kill(info.pid, 'SIGKILL');
|
|
||||||
} catch {
|
} catch {
|
||||||
// Process already dead
|
try {
|
||||||
|
process.kill(info.pid, 'SIGKILL');
|
||||||
|
} catch {
|
||||||
|
// Process already dead
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.removePidFile();
|
this.removePidFile();
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async restart(port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
|
static async restart(port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
@@ -229,6 +255,66 @@ export class ProcessManager {
|
|||||||
return alive;
|
return alive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get worker port from settings file
|
||||||
|
*/
|
||||||
|
private static getPortFromSettings(): number {
|
||||||
|
try {
|
||||||
|
const settingsPath = join(DATA_DIR, 'settings.json');
|
||||||
|
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||||
|
return parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
||||||
|
} catch {
|
||||||
|
return parseInt(SettingsDefaultsManager.get('CLAUDE_MEM_WORKER_PORT'), 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to shut down the worker via HTTP endpoint
|
||||||
|
* Returns true if shutdown succeeded, false if worker not responding
|
||||||
|
*/
|
||||||
|
private static async tryHttpShutdown(port: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Send shutdown request
|
||||||
|
const response = await fetch(`http://127.0.0.1:${port}/api/admin/shutdown`, {
|
||||||
|
method: 'POST',
|
||||||
|
signal: AbortSignal.timeout(2000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for worker to actually stop responding
|
||||||
|
return await this.waitForWorkerDown(port, 5000);
|
||||||
|
} catch {
|
||||||
|
// Worker not responding to HTTP - it may be dead or hung
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for worker to stop responding on the given port
|
||||||
|
*/
|
||||||
|
private static async waitForWorkerDown(port: number, timeout: number): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
try {
|
||||||
|
await fetch(`http://127.0.0.1:${port}/api/health`, {
|
||||||
|
signal: AbortSignal.timeout(500)
|
||||||
|
});
|
||||||
|
// Still responding, wait and retry
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
} catch {
|
||||||
|
// Worker stopped responding - success
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout - worker still responding
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
private static getPidInfo(): PidInfo | null {
|
private static getPidInfo(): PidInfo | null {
|
||||||
try {
|
try {
|
||||||
@@ -237,10 +323,15 @@ export class ProcessManager {
|
|||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
// Validate required fields have correct types
|
// Validate required fields have correct types
|
||||||
if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number') {
|
if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number') {
|
||||||
|
logger.warn('PROCESS', 'Malformed PID file: missing or invalid pid/port fields', {}, { parsed });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parsed as PidInfo;
|
return parsed as PidInfo;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logger.warn('PROCESS', 'Failed to read PID file', {}, {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
path: PID_FILE
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +360,7 @@ export class ProcessManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async waitForHealth(pid: number, port: number, timeoutMs: number = HEALTH_CHECK_TIMEOUT_MS): Promise<{ success: boolean; pid?: number; error?: string }> {
|
private static async waitForHealth(pid: number, port: number, timeoutMs: number = 10000): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
// Increase timeout on Windows to account for slower process startup
|
// Increase timeout on Windows to account for slower process startup
|
||||||
@@ -287,7 +378,7 @@ export class ProcessManager {
|
|||||||
// Try readiness check (changed from /health to /api/readiness)
|
// Try readiness check (changed from /health to /api/readiness)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||||
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
signal: AbortSignal.timeout(1000)
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return { success: true, pid };
|
return { success: true, pid };
|
||||||
@@ -296,7 +387,7 @@ export class ProcessManager {
|
|||||||
// Not ready yet, continue polling
|
// Not ready yet, continue polling
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMsg = isWindows
|
const timeoutMsg = isWindows
|
||||||
@@ -313,7 +404,7 @@ export class ProcessManager {
|
|||||||
if (!this.isProcessAlive(pid)) {
|
if (!this.isProcessAlive(pid)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, PROCESS_EXIT_CHECK_INTERVAL_MS));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Process did not exit within timeout');
|
throw new Error('Process did not exit within timeout');
|
||||||
|
|||||||
@@ -475,8 +475,7 @@ export class WorkerService {
|
|||||||
logger.info('SYSTEM', 'Background initialization complete');
|
logger.info('SYSTEM', 'Background initialization complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('SYSTEM', 'Background initialization failed', {}, error as Error);
|
logger.error('SYSTEM', 'Background initialization failed', {}, error as Error);
|
||||||
// Still resolve to prevent hanging requests, but they'll see searchRoutes is null
|
// Don't resolve - let the promise remain pending so readiness check continues to fail
|
||||||
this.resolveInitialization();
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
*
|
*
|
||||||
* This wrapper exists to solve the Windows zombie port problem.
|
* This wrapper exists to solve the Windows zombie port problem.
|
||||||
* The wrapper spawns the actual worker as a child process.
|
* The wrapper spawns the actual worker as a child process.
|
||||||
* When restart/shutdown is requested, the wrapper kills the child
|
* When shutdown is requested, the wrapper kills the child and exits.
|
||||||
* and respawns it (or exits), ensuring clean socket cleanup.
|
* The hooks will start a fresh wrapper+worker if needed.
|
||||||
*
|
*
|
||||||
* The wrapper itself has no sockets, so Bun's socket cleanup bug
|
* The wrapper itself has no sockets, so Bun's socket cleanup bug
|
||||||
* doesn't affect it.
|
* doesn't affect it.
|
||||||
|
*
|
||||||
|
* NOTE: The wrapper does NOT auto-restart the worker on crash.
|
||||||
|
* This is intentional - the hooks handle startup via ensureWorkerRunning().
|
||||||
|
* Auto-restart would cause PID file mismatches and potential infinite loops.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, ChildProcess, execSync } from 'child_process';
|
import { spawn, ChildProcess, execSync } from 'child_process';
|
||||||
@@ -51,10 +55,11 @@ function spawnInner() {
|
|||||||
log(`Inner exited with code=${code}, signal=${signal}`);
|
log(`Inner exited with code=${code}, signal=${signal}`);
|
||||||
inner = null;
|
inner = null;
|
||||||
|
|
||||||
// If inner crashed unexpectedly (not during shutdown), respawn it
|
// Don't auto-restart - let hooks handle it via ensureWorkerRunning()
|
||||||
if (!isShuttingDown && code !== 0) {
|
// Auto-restart causes PID file mismatches and potential infinite loops
|
||||||
log('Inner crashed, respawning in 1 second...');
|
if (!isShuttingDown) {
|
||||||
setTimeout(() => spawnInner(), 1000);
|
log('Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)');
|
||||||
|
process.exit(code ?? 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { FormattingService } from './FormattingService.js';
|
|||||||
import { TimelineService, TimelineItem } from './TimelineService.js';
|
import { TimelineService, TimelineItem } from './TimelineService.js';
|
||||||
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
|
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
|
||||||
import { logger } from '../../utils/logger.js';
|
import { logger } from '../../utils/logger.js';
|
||||||
import { formatDate, formatTime, extractFirstFile, groupByDate } from '../../shared/timeline-formatting.js';
|
import { formatDate, formatTime, formatDateTime, extractFirstFile, groupByDate, estimateTokens } from '../../shared/timeline-formatting.js';
|
||||||
|
|
||||||
const COLLECTION_NAME = 'cm__claude-mem';
|
const COLLECTION_NAME = 'cm__claude-mem';
|
||||||
const RECENCY_WINDOW_DAYS = 90;
|
const RECENCY_WINDOW_DAYS = 90;
|
||||||
@@ -91,6 +91,7 @@ export class SearchManager {
|
|||||||
let observations: ObservationSearchResult[] = [];
|
let observations: ObservationSearchResult[] = [];
|
||||||
let sessions: SessionSummarySearchResult[] = [];
|
let sessions: SessionSummarySearchResult[] = [];
|
||||||
let prompts: UserPromptSearchResult[] = [];
|
let prompts: UserPromptSearchResult[] = [];
|
||||||
|
let chromaFailed = false;
|
||||||
|
|
||||||
// Determine which types to query based on type filter
|
// Determine which types to query based on type filter
|
||||||
const searchObservations = !type || type === 'observations';
|
const searchObservations = !type || type === 'observations';
|
||||||
@@ -181,17 +182,19 @@ export class SearchManager {
|
|||||||
logger.debug('SEARCH', 'ChromaDB found no matches (final result, no FTS5 fallback)', {});
|
logger.debug('SEARCH', 'ChromaDB found no matches (final result, no FTS5 fallback)', {});
|
||||||
}
|
}
|
||||||
} catch (chromaError: any) {
|
} catch (chromaError: any) {
|
||||||
logger.debug('SEARCH', 'ChromaDB failed - returning empty results (FTS5 fallback removed)', { error: chromaError.message });
|
chromaFailed = true;
|
||||||
|
logger.debug('SEARCH', 'ChromaDB failed - semantic search unavailable', { error: chromaError.message });
|
||||||
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
|
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
|
||||||
// Return empty results - no fallback
|
// Set empty results - will show error message to user
|
||||||
observations = [];
|
observations = [];
|
||||||
sessions = [];
|
sessions = [];
|
||||||
prompts = [];
|
prompts = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ChromaDB not initialized - return empty results (no fallback)
|
// ChromaDB not initialized - mark as failed to show proper error message
|
||||||
else {
|
else if (query) {
|
||||||
logger.debug('SEARCH', 'ChromaDB not initialized - returning empty results (FTS5 fallback removed)', {});
|
chromaFailed = true;
|
||||||
|
logger.debug('SEARCH', 'ChromaDB not initialized - semantic search unavailable', {});
|
||||||
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
|
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
|
||||||
observations = [];
|
observations = [];
|
||||||
sessions = [];
|
sessions = [];
|
||||||
@@ -212,6 +215,14 @@ export class SearchManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalResults === 0) {
|
if (totalResults === 0) {
|
||||||
|
if (chromaFailed) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `⚠️ Vector search failed - semantic search unavailable.\n\nTo enable semantic search:\n1. Install uv: https://docs.astral.sh/uv/getting-started/installation/\n2. Restart the worker: npm run worker:restart\n\nNote: You can still use filter-only searches (date ranges, types, files) without a query term.`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
@@ -484,41 +495,6 @@ export class SearchManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format timeline (helper functions)
|
|
||||||
const formatDate = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const estimateTokens = (text: string | null): number => {
|
|
||||||
if (!text) return 0;
|
|
||||||
return Math.ceil(text.length / 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format results
|
// Format results
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
@@ -1603,41 +1579,6 @@ export class SearchManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions matching context-hook.ts
|
|
||||||
const formatDate = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const estimateTokens = (text: string | null): number => {
|
|
||||||
if (!text) return 0;
|
|
||||||
return Math.ceil(text.length / 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format results matching context-hook.ts exactly
|
// Format results matching context-hook.ts exactly
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
@@ -1893,41 +1834,6 @@ export class SearchManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions (reused from get_context_timeline)
|
|
||||||
const formatDate = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (epochMs: number): string => {
|
|
||||||
const date = new Date(epochMs);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const estimateTokens = (text: string | null): number => {
|
|
||||||
if (!text) return 0;
|
|
||||||
return Math.ceil(text.length / 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format timeline (reused from get_context_timeline)
|
// Format timeline (reused from get_context_timeline)
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ export function createMiddleware(
|
|||||||
// HTTP request/response logging
|
// HTTP request/response logging
|
||||||
middlewares.push((req: Request, res: Response, next: NextFunction) => {
|
middlewares.push((req: Request, res: Response, next: NextFunction) => {
|
||||||
// Skip logging for static assets and health checks
|
// Skip logging for static assets and health checks
|
||||||
if (req.path.startsWith('/health') || req.path === '/' || req.path.includes('.')) {
|
const staticExtensions = ['.html', '.js', '.css', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.woff', '.woff2', '.ttf', '.eot'];
|
||||||
|
const isStaticAsset = staticExtensions.some(ext => req.path.endsWith(ext));
|
||||||
|
if (req.path.startsWith('/health') || req.path === '/' || isStaticAsset) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ export class DataRoutes extends BaseRouteHandler {
|
|||||||
const queueDepth = this.sessionManager.getTotalQueueDepth();
|
const queueDepth = this.sessionManager.getTotalQueueDepth();
|
||||||
const activeSessions = this.sessionManager.getActiveSessionCount();
|
const activeSessions = this.sessionManager.getActiveSessionCount();
|
||||||
|
|
||||||
res.json({ status: 'ok', isProcessing });
|
res.json({ status: 'ok', isProcessing, queueDepth, activeSessions });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -277,19 +277,14 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
// Skip meta-observations: file operations on session-memory files
|
// Skip meta-observations: file operations on session-memory files
|
||||||
const fileOperationTools = new Set(['Edit', 'Write', 'Read', 'NotebookEdit']);
|
const fileOperationTools = new Set(['Edit', 'Write', 'Read', 'NotebookEdit']);
|
||||||
if (fileOperationTools.has(tool_name) && tool_input) {
|
if (fileOperationTools.has(tool_name) && tool_input) {
|
||||||
try {
|
const filePath = tool_input.file_path || tool_input.notebook_path;
|
||||||
const filePath = tool_input.file_path || tool_input.notebook_path;
|
if (filePath && filePath.includes('session-memory')) {
|
||||||
if (filePath && filePath.includes('session-memory')) {
|
logger.debug('SESSION', 'Skipping meta-observation for session-memory file', {
|
||||||
logger.debug('SESSION', 'Skipping meta-observation for session-memory file', {
|
tool_name,
|
||||||
tool_name,
|
file_path: filePath
|
||||||
file_path: filePath
|
});
|
||||||
});
|
res.json({ status: 'skipped', reason: 'session_memory_meta' });
|
||||||
res.json({ status: 'skipped', reason: 'session_memory_meta' });
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If we can't parse tool_input, continue normally
|
|
||||||
logger.debug('SESSION', 'Could not check file_path for session-memory filter', { tool_name }, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,70 +59,8 @@ export class SettingsRoutes extends BaseRouteHandler {
|
|||||||
* Update environment settings (in ~/.claude-mem/settings.json) with validation
|
* Update environment settings (in ~/.claude-mem/settings.json) with validation
|
||||||
*/
|
*/
|
||||||
private handleUpdateSettings = this.wrapHandler((req: Request, res: Response): void => {
|
private handleUpdateSettings = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
// Validate all settings
|
||||||
if (req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
const validation = this.validateSettings(req.body);
|
||||||
const obsCount = parseInt(req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
|
||||||
if (isNaN(obsCount) || obsCount < 1 || obsCount > 200) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate CLAUDE_MEM_WORKER_PORT
|
|
||||||
if (req.body.CLAUDE_MEM_WORKER_PORT) {
|
|
||||||
const port = parseInt(req.body.CLAUDE_MEM_WORKER_PORT, 10);
|
|
||||||
if (isNaN(port) || port < 1024 || port > 65535) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate CLAUDE_MEM_WORKER_HOST (IP address or 0.0.0.0)
|
|
||||||
if (req.body.CLAUDE_MEM_WORKER_HOST) {
|
|
||||||
const host = req.body.CLAUDE_MEM_WORKER_HOST;
|
|
||||||
// Allow localhost variants and valid IP patterns
|
|
||||||
const validHostPattern = /^(127\.0\.0\.1|0\.0\.0\.0|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
|
|
||||||
if (!validHostPattern.test(host)) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'CLAUDE_MEM_WORKER_HOST must be a valid IP address (e.g., 127.0.0.1, 0.0.0.0)'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate CLAUDE_MEM_LOG_LEVEL
|
|
||||||
if (req.body.CLAUDE_MEM_LOG_LEVEL) {
|
|
||||||
const validLevels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'SILENT'];
|
|
||||||
if (!validLevels.includes(req.body.CLAUDE_MEM_LOG_LEVEL.toUpperCase())) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'CLAUDE_MEM_LOG_LEVEL must be one of: DEBUG, INFO, WARN, ERROR, SILENT'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate CLAUDE_MEM_PYTHON_VERSION (must be valid Python version format)
|
|
||||||
if (req.body.CLAUDE_MEM_PYTHON_VERSION) {
|
|
||||||
const pythonVersionRegex = /^3\.\d{1,2}$/;
|
|
||||||
if (!pythonVersionRegex.test(req.body.CLAUDE_MEM_PYTHON_VERSION)) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'CLAUDE_MEM_PYTHON_VERSION must be in format "3.X" or "3.XX" (e.g., "3.13")'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate context settings
|
|
||||||
const validation = this.validateContextSettings(req.body);
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -274,9 +212,51 @@ export class SettingsRoutes extends BaseRouteHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate context settings from request body
|
* Validate all settings from request body (single source of truth)
|
||||||
*/
|
*/
|
||||||
private validateContextSettings(settings: any): { valid: boolean; error?: string } {
|
private validateSettings(settings: any): { valid: boolean; error?: string } {
|
||||||
|
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
||||||
|
if (settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||||
|
const obsCount = parseInt(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||||
|
if (isNaN(obsCount) || obsCount < 1 || obsCount > 200) {
|
||||||
|
return { valid: false, error: 'CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CLAUDE_MEM_WORKER_PORT
|
||||||
|
if (settings.CLAUDE_MEM_WORKER_PORT) {
|
||||||
|
const port = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
||||||
|
if (isNaN(port) || port < 1024 || port > 65535) {
|
||||||
|
return { valid: false, error: 'CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CLAUDE_MEM_WORKER_HOST (IP address or 0.0.0.0)
|
||||||
|
if (settings.CLAUDE_MEM_WORKER_HOST) {
|
||||||
|
const host = settings.CLAUDE_MEM_WORKER_HOST;
|
||||||
|
// Allow localhost variants and valid IP patterns
|
||||||
|
const validHostPattern = /^(127\.0\.0\.1|0\.0\.0\.0|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
|
||||||
|
if (!validHostPattern.test(host)) {
|
||||||
|
return { valid: false, error: 'CLAUDE_MEM_WORKER_HOST must be a valid IP address (e.g., 127.0.0.1, 0.0.0.0)' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CLAUDE_MEM_LOG_LEVEL
|
||||||
|
if (settings.CLAUDE_MEM_LOG_LEVEL) {
|
||||||
|
const validLevels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'SILENT'];
|
||||||
|
if (!validLevels.includes(settings.CLAUDE_MEM_LOG_LEVEL.toUpperCase())) {
|
||||||
|
return { valid: false, error: 'CLAUDE_MEM_LOG_LEVEL must be one of: DEBUG, INFO, WARN, ERROR, SILENT' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CLAUDE_MEM_PYTHON_VERSION (must be valid Python version format)
|
||||||
|
if (settings.CLAUDE_MEM_PYTHON_VERSION) {
|
||||||
|
const pythonVersionRegex = /^3\.\d{1,2}$/;
|
||||||
|
if (!pythonVersionRegex.test(settings.CLAUDE_MEM_PYTHON_VERSION)) {
|
||||||
|
return { valid: false, error: 'CLAUDE_MEM_PYTHON_VERSION must be in format "3.X" or "3.XX" (e.g., "3.13")' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate boolean string values
|
// Validate boolean string values
|
||||||
const booleanSettings = [
|
const booleanSettings = [
|
||||||
'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS',
|
'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS',
|
||||||
|
|||||||
@@ -104,39 +104,45 @@ export class SettingsDefaultsManager {
|
|||||||
/**
|
/**
|
||||||
* Load settings from file with fallback to defaults
|
* Load settings from file with fallback to defaults
|
||||||
* Returns merged settings with defaults as fallback
|
* Returns merged settings with defaults as fallback
|
||||||
|
* Handles all errors (missing file, corrupted JSON, permissions) by returning defaults
|
||||||
*/
|
*/
|
||||||
static loadFromFile(settingsPath: string): SettingsDefaults {
|
static loadFromFile(settingsPath: string): SettingsDefaults {
|
||||||
if (!existsSync(settingsPath)) {
|
try {
|
||||||
|
if (!existsSync(settingsPath)) {
|
||||||
|
return this.getAllDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsData = readFileSync(settingsPath, 'utf-8');
|
||||||
|
const settings = JSON.parse(settingsData);
|
||||||
|
|
||||||
|
// MIGRATION: Handle old nested schema { env: {...} }
|
||||||
|
let flatSettings = settings;
|
||||||
|
if (settings.env && typeof settings.env === 'object') {
|
||||||
|
// Migrate from nested to flat schema
|
||||||
|
flatSettings = settings.env;
|
||||||
|
|
||||||
|
// Auto-migrate the file to flat schema
|
||||||
|
try {
|
||||||
|
writeFileSync(settingsPath, JSON.stringify(flatSettings, null, 2), 'utf-8');
|
||||||
|
logger.info('SETTINGS', 'Migrated settings file from nested to flat schema', { settingsPath });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('SETTINGS', 'Failed to auto-migrate settings file', { settingsPath }, error);
|
||||||
|
// Continue with in-memory migration even if write fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge file settings with defaults (flat schema)
|
||||||
|
const result: SettingsDefaults = { ...this.DEFAULTS };
|
||||||
|
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
|
||||||
|
if (flatSettings[key] !== undefined) {
|
||||||
|
result[key] = flatSettings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('SETTINGS', 'Failed to load settings, using defaults', { settingsPath }, error);
|
||||||
return this.getAllDefaults();
|
return this.getAllDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsData = readFileSync(settingsPath, 'utf-8');
|
|
||||||
const settings = JSON.parse(settingsData);
|
|
||||||
|
|
||||||
// MIGRATION: Handle old nested schema { env: {...} }
|
|
||||||
let flatSettings = settings;
|
|
||||||
if (settings.env && typeof settings.env === 'object') {
|
|
||||||
// Migrate from nested to flat schema
|
|
||||||
flatSettings = settings.env;
|
|
||||||
|
|
||||||
// Auto-migrate the file to flat schema
|
|
||||||
try {
|
|
||||||
writeFileSync(settingsPath, JSON.stringify(flatSettings, null, 2), 'utf-8');
|
|
||||||
logger.info('SETTINGS', 'Migrated settings file from nested to flat schema', { settingsPath });
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('SETTINGS', 'Failed to auto-migrate settings file', { settingsPath }, error);
|
|
||||||
// Continue with in-memory migration even if write fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge file settings with defaults (flat schema)
|
|
||||||
const result: SettingsDefaults = { ...this.DEFAULTS };
|
|
||||||
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
|
|
||||||
if (flatSettings[key] !== undefined) {
|
|
||||||
result[key] = flatSettings[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ export function parseJsonArray(json: string | null): string[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format date with time (e.g., "Dec 14, 7:30 PM")
|
* Format date with time (e.g., "Dec 14, 7:30 PM")
|
||||||
|
* Accepts either ISO date string or epoch milliseconds
|
||||||
*/
|
*/
|
||||||
export function formatDateTime(dateStr: string): string {
|
export function formatDateTime(dateInput: string | number): string {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateInput);
|
||||||
return date.toLocaleString('en-US', {
|
return date.toLocaleString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -36,9 +37,10 @@ export function formatDateTime(dateStr: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format just time, no date (e.g., "7:30 PM")
|
* Format just time, no date (e.g., "7:30 PM")
|
||||||
|
* Accepts either ISO date string or epoch milliseconds
|
||||||
*/
|
*/
|
||||||
export function formatTime(dateStr: string): string {
|
export function formatTime(dateInput: string | number): string {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateInput);
|
||||||
return date.toLocaleString('en-US', {
|
return date.toLocaleString('en-US', {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@@ -48,9 +50,10 @@ export function formatTime(dateStr: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format just date (e.g., "Dec 14, 2025")
|
* Format just date (e.g., "Dec 14, 2025")
|
||||||
|
* Accepts either ISO date string or epoch milliseconds
|
||||||
*/
|
*/
|
||||||
export function formatDate(dateStr: string): string {
|
export function formatDate(dateInput: string | number): string {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateInput);
|
||||||
return date.toLocaleString('en-US', {
|
return date.toLocaleString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -76,6 +79,14 @@ export function extractFirstFile(filesModified: string | null, cwd: string): str
|
|||||||
return files.length > 0 ? toRelativePath(files[0], cwd) : 'General';
|
return files.length > 0 ? toRelativePath(files[0], cwd) : 'General';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate token count for text (rough approximation: ~4 chars per token)
|
||||||
|
*/
|
||||||
|
export function estimateTokens(text: string | null): number {
|
||||||
|
if (!text) return 0;
|
||||||
|
return Math.ceil(text.length / 4);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group items by date
|
* Group items by date
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -56,6 +56,20 @@ export function extractLastMessage(
|
|||||||
.filter((c: any) => c.type === 'text')
|
.filter((c: any) => c.type === 'text')
|
||||||
.map((c: any) => c.text)
|
.map((c: any) => c.text)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
} else {
|
||||||
|
// Unknown content format - log error and skip this message
|
||||||
|
logger.error(
|
||||||
|
'PARSER',
|
||||||
|
'Unknown message content format',
|
||||||
|
{
|
||||||
|
role,
|
||||||
|
transcriptPath,
|
||||||
|
contentType: typeof msgContent,
|
||||||
|
content: msgContent
|
||||||
|
},
|
||||||
|
new Error('Message content is neither string nor array')
|
||||||
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stripSystemReminders) {
|
if (stripSystemReminders) {
|
||||||
|
|||||||
+24
-23
@@ -13,8 +13,9 @@ const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplace
|
|||||||
// Named constants for health checks
|
// Named constants for health checks
|
||||||
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
|
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
|
||||||
|
|
||||||
// Port cache to avoid repeated settings file reads
|
// Cache to avoid repeated settings file reads
|
||||||
let cachedPort: number | null = null;
|
let cachedPort: number | null = null;
|
||||||
|
let cachedHost: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the worker port number from settings
|
* Get the worker port number from settings
|
||||||
@@ -26,35 +27,35 @@ export function getWorkerPort(): number {
|
|||||||
return cachedPort;
|
return cachedPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const settingsPath = path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), 'settings.json');
|
||||||
const settingsPath = path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), 'settings.json');
|
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
cachedPort = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
||||||
cachedPort = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
return cachedPort;
|
||||||
return cachedPort;
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback to default if settings load fails
|
|
||||||
logger.debug('SYSTEM', 'Failed to load port from settings, using default', { error });
|
|
||||||
cachedPort = parseInt(SettingsDefaultsManager.get('CLAUDE_MEM_WORKER_PORT'), 10);
|
|
||||||
return cachedPort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the cached port value
|
|
||||||
* Call this when settings are updated to force re-reading from file
|
|
||||||
*/
|
|
||||||
export function clearPortCache(): void {
|
|
||||||
cachedPort = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the worker host address
|
* Get the worker host address
|
||||||
* Priority: ~/.claude-mem/settings.json > env var > default (127.0.0.1)
|
* Uses CLAUDE_MEM_WORKER_HOST from settings file or default (127.0.0.1)
|
||||||
|
* Caches the host value to avoid repeated file reads
|
||||||
*/
|
*/
|
||||||
export function getWorkerHost(): string {
|
export function getWorkerHost(): string {
|
||||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
if (cachedHost !== null) {
|
||||||
|
return cachedHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPath = path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), 'settings.json');
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||||
return settings.CLAUDE_MEM_WORKER_HOST;
|
cachedHost = settings.CLAUDE_MEM_WORKER_HOST;
|
||||||
|
return cachedHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cached port and host values
|
||||||
|
* Call this when settings are updated to force re-reading from file
|
||||||
|
*/
|
||||||
|
export function clearPortCache(): void {
|
||||||
|
cachedPort = null;
|
||||||
|
cachedHost = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,18 +24,6 @@ export function getWorkerRestartInstructions(
|
|||||||
actualError
|
actualError
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const isWindows = process.platform === 'win32';
|
|
||||||
|
|
||||||
// Platform-specific directory paths
|
|
||||||
const pluginDir = isWindows
|
|
||||||
? '%USERPROFILE%\\.claude\\plugins\\marketplaces\\thedotmack'
|
|
||||||
: '~/.claude/plugins/marketplaces/thedotmack';
|
|
||||||
|
|
||||||
// Platform-specific terminal name
|
|
||||||
const terminal = isWindows
|
|
||||||
? 'Command Prompt or PowerShell'
|
|
||||||
: 'Terminal';
|
|
||||||
|
|
||||||
// Build error message
|
// Build error message
|
||||||
const prefix = customPrefix || 'Worker service connection failed.';
|
const prefix = customPrefix || 'Worker service connection failed.';
|
||||||
const portInfo = port ? ` (port ${port})` : '';
|
const portInfo = port ? ` (port ${port})` : '';
|
||||||
@@ -43,10 +31,8 @@ export function getWorkerRestartInstructions(
|
|||||||
let message = `${prefix}${portInfo}\n\n`;
|
let message = `${prefix}${portInfo}\n\n`;
|
||||||
message += `To restart the worker:\n`;
|
message += `To restart the worker:\n`;
|
||||||
message += `1. Exit Claude Code completely\n`;
|
message += `1. Exit Claude Code completely\n`;
|
||||||
message += `2. Open ${terminal}\n`;
|
message += `2. Run: claude-mem restart\n`;
|
||||||
message += `3. Navigate to: ${pluginDir}\n`;
|
message += `3. Restart Claude Code`;
|
||||||
message += `4. Run: npm run worker:restart\n`;
|
|
||||||
message += `5. Restart Claude Code`;
|
|
||||||
|
|
||||||
if (includeSkillFallback) {
|
if (includeSkillFallback) {
|
||||||
message += `\n\nIf that doesn't work, try: /troubleshoot`;
|
message += `\n\nIf that doesn't work, try: /troubleshoot`;
|
||||||
|
|||||||
+45
-14
@@ -101,27 +101,58 @@ class Logger {
|
|||||||
try {
|
try {
|
||||||
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
|
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
|
||||||
|
|
||||||
// Special formatting for common tools
|
// Bash: show full command
|
||||||
if (toolName === 'Bash' && input.command) {
|
if (toolName === 'Bash' && input.command) {
|
||||||
const cmd = input.command.length > 50
|
return `${toolName}(${input.command})`;
|
||||||
? input.command.substring(0, 50) + '...'
|
|
||||||
: input.command;
|
|
||||||
return `${toolName}(${cmd})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolName === 'Read' && input.file_path) {
|
// File operations: show full path
|
||||||
const path = input.file_path.split('/').pop() || input.file_path;
|
if (input.file_path) {
|
||||||
return `${toolName}(${path})`;
|
return `${toolName}(${input.file_path})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolName === 'Edit' && input.file_path) {
|
// NotebookEdit: show full notebook path
|
||||||
const path = input.file_path.split('/').pop() || input.file_path;
|
if (input.notebook_path) {
|
||||||
return `${toolName}(${path})`;
|
return `${toolName}(${input.notebook_path})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolName === 'Write' && input.file_path) {
|
// Glob: show full pattern
|
||||||
const path = input.file_path.split('/').pop() || input.file_path;
|
if (toolName === 'Glob' && input.pattern) {
|
||||||
return `${toolName}(${path})`;
|
return `${toolName}(${input.pattern})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grep: show full pattern
|
||||||
|
if (toolName === 'Grep' && input.pattern) {
|
||||||
|
return `${toolName}(${input.pattern})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebFetch/WebSearch: show full URL or query
|
||||||
|
if (input.url) {
|
||||||
|
return `${toolName}(${input.url})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.query) {
|
||||||
|
return `${toolName}(${input.query})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task: show subagent_type or full description
|
||||||
|
if (toolName === 'Task') {
|
||||||
|
if (input.subagent_type) {
|
||||||
|
return `${toolName}(${input.subagent_type})`;
|
||||||
|
}
|
||||||
|
if (input.description) {
|
||||||
|
return `${toolName}(${input.description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill: show skill name
|
||||||
|
if (toolName === 'Skill' && input.skill) {
|
||||||
|
return `${toolName}(${input.skill})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LSP: show operation type
|
||||||
|
if (toolName === 'LSP' && input.operation) {
|
||||||
|
return `${toolName}(${input.operation})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: just show tool name
|
// Default: just show tool name
|
||||||
|
|||||||
@@ -22,15 +22,17 @@ export function getProjectName(cwd: string | null | undefined): string {
|
|||||||
if (basename === '') {
|
if (basename === '') {
|
||||||
// Extract drive letter on Windows, or use 'root' on Unix
|
// Extract drive letter on Windows, or use 'root' on Unix
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
if (isWindows && cwd.match(/^[A-Z]:\\/i)) {
|
if (isWindows) {
|
||||||
const driveLetter = cwd[0].toUpperCase();
|
const driveMatch = cwd.match(/^([A-Z]):\\/i);
|
||||||
const projectName = `drive-${driveLetter}`;
|
if (driveMatch) {
|
||||||
logger.info('PROJECT_NAME', 'Drive root detected', { cwd, projectName });
|
const driveLetter = driveMatch[1].toUpperCase();
|
||||||
return projectName;
|
const projectName = `drive-${driveLetter}`;
|
||||||
} else {
|
logger.info('PROJECT_NAME', 'Drive root detected', { cwd, projectName });
|
||||||
logger.warn('PROJECT_NAME', 'Root directory detected, using fallback', { cwd });
|
return projectName;
|
||||||
return 'unknown-project';
|
}
|
||||||
}
|
}
|
||||||
|
logger.warn('PROJECT_NAME', 'Root directory detected, using fallback', { cwd });
|
||||||
|
return 'unknown-project';
|
||||||
}
|
}
|
||||||
|
|
||||||
return basename;
|
return basename;
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Happy Path Error With Fallback
|
|
||||||
*
|
|
||||||
* @deprecated This function is deprecated. Use logger.happyPathError() instead.
|
|
||||||
* All usages have been migrated to the new logger system which consolidates logs
|
|
||||||
* into the regular worker logs instead of separate silent.log files.
|
|
||||||
*
|
|
||||||
* Migration example:
|
|
||||||
* OLD: happy_path_error__with_fallback('Missing value', { data }, 'default')
|
|
||||||
* NEW: logger.happyPathError('COMPONENT', 'Missing value', undefined, { data }, 'default')
|
|
||||||
*
|
|
||||||
* See: src/utils/logger.ts for the new happyPathError method
|
|
||||||
* Issue: #312 - Consolidate silent logs into regular worker logs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { appendFileSync } from 'fs';
|
|
||||||
import { homedir } from 'os';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
const LOG_FILE = join(homedir(), '.claude-mem', 'silent.log');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write an error message to silent.log and return fallback value
|
|
||||||
* @param message - Error message describing what went wrong
|
|
||||||
* @param data - Optional data to include (will be JSON stringified)
|
|
||||||
* @param fallback - Value to return (defaults to empty string)
|
|
||||||
* @returns The fallback value (for use in || fallbacks)
|
|
||||||
*/
|
|
||||||
export function happy_path_error__with_fallback(message: string, data?: any, fallback: string = ''): string {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
// Capture stack trace to get caller location
|
|
||||||
const stack = new Error().stack || '';
|
|
||||||
const stackLines = stack.split('\n');
|
|
||||||
// Line 0: "Error"
|
|
||||||
// Line 1: "at silentDebug ..."
|
|
||||||
// Line 2: "at <CALLER> ..." <- We want this one
|
|
||||||
const callerLine = stackLines[2] || '';
|
|
||||||
const callerMatch = callerLine.match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/);
|
|
||||||
const location = callerMatch
|
|
||||||
? `${callerMatch[1].split('/').pop()}:${callerMatch[2]}`
|
|
||||||
: 'unknown';
|
|
||||||
|
|
||||||
let logLine = `[${timestamp}] [HAPPY-PATH-ERROR] [${location}] ${message}`;
|
|
||||||
|
|
||||||
if (data !== undefined) {
|
|
||||||
try {
|
|
||||||
logLine += ` ${JSON.stringify(data)}`;
|
|
||||||
} catch (error) {
|
|
||||||
logLine += ` [stringify error: ${error}]`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logLine += '\n';
|
|
||||||
|
|
||||||
try {
|
|
||||||
appendFileSync(LOG_FILE, logLine);
|
|
||||||
} catch (error) {
|
|
||||||
// If we can't write to the log file, fail silently (it's a debug utility after all)
|
|
||||||
// Only write to stderr as a last resort
|
|
||||||
console.error('[silent-debug] Failed to write to log:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the silent log file
|
|
||||||
*/
|
|
||||||
export function clearSilentLog(): void {
|
|
||||||
try {
|
|
||||||
appendFileSync(LOG_FILE, `\n${'='.repeat(80)}\n[${new Date().toISOString()}] Log cleared\n${'='.repeat(80)}\n\n`);
|
|
||||||
} catch (error) {
|
|
||||||
// Expected: Log file may not be writable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+15
-37
@@ -31,20 +31,10 @@ function countTags(content: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip memory tags from JSON-serialized content (tool inputs/responses)
|
* Internal function to strip memory tags from content
|
||||||
*
|
* Shared logic extracted from both JSON and prompt stripping functions
|
||||||
* @param content - Stringified JSON content from tool_input or tool_response
|
|
||||||
* @returns Cleaned content with tags removed, or '{}' if non-string/invalid
|
|
||||||
*
|
|
||||||
* Note: Returns '{}' for non-strings because this is used in JSON context
|
|
||||||
* where we need a valid JSON object if the input is invalid.
|
|
||||||
*/
|
*/
|
||||||
export function stripMemoryTagsFromJson(content: string): string {
|
function stripTagsInternal(content: string): string {
|
||||||
if (typeof content !== 'string') {
|
|
||||||
logger.happyPathError('SYSTEM', 'received non-string for JSON context', undefined, { type: typeof content }, '{}');
|
|
||||||
return '{}'; // Safe default for JSON context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReDoS protection: limit tag count before regex processing
|
// ReDoS protection: limit tag count before regex processing
|
||||||
const tagCount = countTags(content);
|
const tagCount = countTags(content);
|
||||||
if (tagCount > MAX_TAG_COUNT) {
|
if (tagCount > MAX_TAG_COUNT) {
|
||||||
@@ -62,34 +52,22 @@ export function stripMemoryTagsFromJson(content: string): string {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip memory tags from JSON-serialized content (tool inputs/responses)
|
||||||
|
*
|
||||||
|
* @param content - Stringified JSON content from tool_input or tool_response
|
||||||
|
* @returns Cleaned content with tags removed, or '{}' if invalid
|
||||||
|
*/
|
||||||
|
export function stripMemoryTagsFromJson(content: string): string {
|
||||||
|
return stripTagsInternal(content);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip memory tags from user prompt content
|
* Strip memory tags from user prompt content
|
||||||
*
|
*
|
||||||
* @param content - Raw user prompt text
|
* @param content - Raw user prompt text
|
||||||
* @returns Cleaned content with tags removed, or '' if non-string/invalid
|
* @returns Cleaned content with tags removed
|
||||||
*
|
|
||||||
* Note: Returns '' (empty string) for non-strings because this is used in prompt context
|
|
||||||
* where an empty prompt indicates the user didn't provide any content.
|
|
||||||
*/
|
*/
|
||||||
export function stripMemoryTagsFromPrompt(content: string): string {
|
export function stripMemoryTagsFromPrompt(content: string): string {
|
||||||
if (typeof content !== 'string') {
|
return stripTagsInternal(content);
|
||||||
logger.happyPathError('SYSTEM', 'received non-string for prompt context', undefined, { type: typeof content }, '');
|
|
||||||
return ''; // Safe default for prompt content
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReDoS protection: limit tag count before regex processing
|
|
||||||
const tagCount = countTags(content);
|
|
||||||
if (tagCount > MAX_TAG_COUNT) {
|
|
||||||
logger.warn('SYSTEM', 'tag count exceeds limit', undefined, {
|
|
||||||
tagCount,
|
|
||||||
maxAllowed: MAX_TAG_COUNT,
|
|
||||||
contentLength: content.length
|
|
||||||
});
|
|
||||||
// Still process but log the anomaly
|
|
||||||
}
|
|
||||||
|
|
||||||
return content
|
|
||||||
.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g, '')
|
|
||||||
.replace(/<private>[\s\S]*?<\/private>/g, '')
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe('Hook Error Logging', () => {
|
|||||||
handleFetchError(mockResponse, errorText, context);
|
handleFetchError(mockResponse, errorText, context);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
expect(error.message).toContain('Failed Observation storage for Bash');
|
expect(error.message).toContain('Failed Observation storage for Bash');
|
||||||
expect(error.message).toContain('npm run worker:restart');
|
expect(error.message).toContain('claude-mem restart');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ describe('Hook Error Logging', () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
handleWorkerError(connError);
|
handleWorkerError(connError);
|
||||||
}).toThrow('npm run worker:restart');
|
}).toThrow('claude-mem restart');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('re-throws non-connection errors unchanged', () => {
|
it('re-throws non-connection errors unchanged', () => {
|
||||||
@@ -130,7 +130,7 @@ describe('Hook Error Logging', () => {
|
|||||||
expect.fail('Should have thrown');
|
expect.fail('Should have thrown');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
expect(error.message).toBe('Something went wrong');
|
expect(error.message).toBe('Something went wrong');
|
||||||
expect(error.message).not.toContain('npm run worker:restart');
|
expect(error.message).not.toContain('claude-mem restart');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ describe('Hook Error Logging', () => {
|
|||||||
handleFetchError(mockResponse, 'error', context);
|
handleFetchError(mockResponse, 'error', context);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Must include restart command
|
// Must include restart command
|
||||||
expect(error.message).toMatch(/npm run worker:restart/);
|
expect(error.message).toMatch(/claude-mem restart/);
|
||||||
|
|
||||||
// Must be user-facing (no technical jargon)
|
// Must be user-facing (no technical jargon)
|
||||||
expect(error.message).not.toContain('ECONNREFUSED');
|
expect(error.message).not.toContain('ECONNREFUSED');
|
||||||
|
|||||||
Reference in New Issue
Block a user