Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84f2061d8f | |||
| 1391df4fe8 | |||
| 71b29af00a | |||
| 0768fafd83 | |||
| 5ce656037e | |||
| e27f8e4963 | |||
| af145cfaef | |||
| 15fe0cfe3c | |||
| c0ed9bbcfd | |||
| 8040c6d559 | |||
| 7187220b24 | |||
| ca52950b2a | |||
| ee1441f462 | |||
| c4af31f48d | |||
| c2742d5664 | |||
| 0c45919261 | |||
| a3ab898e04 | |||
| dea67c0d86 | |||
| d13a2c237c | |||
| c592f0aa69 | |||
| 85a2472e4e | |||
| 0cb3256b2d | |||
| 44029862b1 | |||
| 130abe04a9 | |||
| bff10d49c9 | |||
| 40a71d3250 |
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.6",
|
"version": "7.4.3",
|
||||||
"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
|
||||||
|
|||||||
+172
-2143
File diff suppressed because it is too large
Load Diff
@@ -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.3.6",
|
"version": "7.4.3",
|
||||||
"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.3.6",
|
"version": "7.4.3",
|
||||||
"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.3.5",
|
"version": "7.4.2",
|
||||||
"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
+24
-12
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
+224
-184
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.
@@ -212,3 +212,118 @@ help(topic="all") # Get complete guide
|
|||||||
- ALWAYS get timeline context to understand what was happening
|
- ALWAYS get timeline context to understand what was happening
|
||||||
- ALWAYS use `get_observations` when fetching 2+ observations
|
- ALWAYS use `get_observations` when fetching 2+ observations
|
||||||
- The workflow is optimized: search → timeline → batch fetch = 10-100x faster
|
- The workflow is optimized: search → timeline → batch fetch = 10-100x faster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Reference
|
||||||
|
|
||||||
|
Comprehensive parameter documentation for all memory tools. For MCP usage, call `help(topic="search")` to load specific tool docs.
|
||||||
|
|
||||||
|
### search
|
||||||
|
|
||||||
|
Search across all memory types (observations, sessions, prompts).
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `query` (string, optional) - Search term for full-text search
|
||||||
|
- `limit` (number, optional) - Maximum results to return. Default: 20, Max: 100
|
||||||
|
- `offset` (number, optional) - Number of results to skip. Default: 0
|
||||||
|
- `project` (string, required) - Project name to filter by
|
||||||
|
- `type` (string, optional) - Filter by type: "observations", "sessions", "prompts"
|
||||||
|
- `dateStart` (string, optional) - Start date filter (YYYY-MM-DD or epoch ms)
|
||||||
|
- `dateEnd` (string, optional) - End date filter (YYYY-MM-DD or epoch ms)
|
||||||
|
- `obs_type` (string, optional) - Filter observations by type (comma-separated): bugfix, feature, decision, discovery, change
|
||||||
|
- `orderBy` (string, optional) - Sort order: "date_desc" (default), "date_asc", "relevance"
|
||||||
|
|
||||||
|
**Returns:** Table of results with IDs, timestamps, types, titles
|
||||||
|
|
||||||
|
### timeline
|
||||||
|
|
||||||
|
Get chronological context around a specific point in time or observation.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `anchor` (number, optional) - Observation ID to center timeline around. If not provided, uses most recent result from query
|
||||||
|
- `query` (string, optional) - Search term to find anchor automatically (if anchor not provided)
|
||||||
|
- `depth_before` (number, optional) - Items before anchor. Default: 5, Max: 20
|
||||||
|
- `depth_after` (number, optional) - Items after anchor. Default: 5, Max: 20
|
||||||
|
- `project` (string, required) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Exactly `depth_before + 1 + depth_after` items in chronological order, with observations, sessions, and prompts interleaved
|
||||||
|
|
||||||
|
### get_recent_context
|
||||||
|
|
||||||
|
Get the most recent observations from current or recent sessions.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `limit` (number, optional) - Maximum observations to return. Default: 10, Max: 50
|
||||||
|
- `project` (string, required) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Recent observations in reverse chronological order
|
||||||
|
|
||||||
|
### get_context_timeline
|
||||||
|
|
||||||
|
Get timeline context around a specific observation ID.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `anchor` (number, required) - Observation ID to center timeline around
|
||||||
|
- `depth_before` (number, optional) - Items before anchor. Default: 5, Max: 20
|
||||||
|
- `depth_after` (number, optional) - Items after anchor. Default: 5, Max: 20
|
||||||
|
- `project` (string, optional) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Timeline items centered on the anchor observation
|
||||||
|
|
||||||
|
### get_observation
|
||||||
|
|
||||||
|
Fetch a single observation by ID with full details.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (number, required) - Observation ID to fetch
|
||||||
|
|
||||||
|
**Returns:** Complete observation object with title, subtitle, narrative, facts, concepts, files, timestamps
|
||||||
|
|
||||||
|
### get_observations
|
||||||
|
|
||||||
|
Batch fetch multiple observations by IDs. Always prefer this over individual fetches for 2+ observations.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `ids` (array of numbers, required) - Array of observation IDs to fetch
|
||||||
|
- `orderBy` (string, optional) - Sort order: "date_desc" (default), "date_asc"
|
||||||
|
- `limit` (number, optional) - Maximum observations to return. Default: no limit
|
||||||
|
- `project` (string, optional) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Array of complete observation objects, 10-100x faster than individual fetches
|
||||||
|
|
||||||
|
### get_session
|
||||||
|
|
||||||
|
Fetch a single session by ID with metadata.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (number, required) - Session ID to fetch (just the number, not "S2005" format)
|
||||||
|
|
||||||
|
**Returns:** Session object with ID, start time, end time, project, model info
|
||||||
|
|
||||||
|
### get_prompt
|
||||||
|
|
||||||
|
Fetch a single prompt by ID with full text.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (number, required) - Prompt ID to fetch
|
||||||
|
|
||||||
|
**Returns:** Prompt object with ID, text, timestamp, session reference
|
||||||
|
|
||||||
|
### help
|
||||||
|
|
||||||
|
Load detailed instructions for specific topics or all documentation.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `topic` (string, optional) - Specific topic to load: "workflow", "search", "timeline", "get_recent_context", "get_context_timeline", "get_observation", "get_observations", "get_session", "get_prompt", "all". Default: "all"
|
||||||
|
|
||||||
|
**Returns:** Formatted documentation for the requested topic
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ async function buildHooks() {
|
|||||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||||
},
|
},
|
||||||
banner: {
|
banner: {
|
||||||
js: '#!/usr/bin/env bun'
|
js: '#!/usr/bin/env node'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
* native module dependencies.
|
* native module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
|
||||||
import { stdin } from "process";
|
import { stdin } from "process";
|
||||||
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";
|
||||||
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";
|
||||||
|
import { getProjectName } from "../utils/project-name.js";
|
||||||
|
|
||||||
export interface SessionStartInput {
|
export interface SessionStartInput {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -25,7 +25,7 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
|
|||||||
await ensureWorkerRunning();
|
await ensureWorkerRunning();
|
||||||
|
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : "unknown-project";
|
const project = getProjectName(cwd);
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
|
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import path from 'path';
|
|
||||||
import { stdin } from 'process';
|
import { stdin } from 'process';
|
||||||
import { createHookResponse } from './hook-response.js';
|
import { createHookResponse } 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';
|
||||||
|
import { getProjectName } from '../utils/project-name.js';
|
||||||
|
|
||||||
export interface UserPromptSubmitInput {
|
export interface UserPromptSubmitInput {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -24,7 +24,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { session_id, cwd, prompt } = input;
|
const { session_id, cwd, prompt } = input;
|
||||||
const project = path.basename(cwd);
|
const project = getProjectName(cwd);
|
||||||
|
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
|
|||||||
+187
-73
@@ -6,14 +6,18 @@
|
|||||||
* 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 {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { z } from 'zod';
|
|
||||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
|
|
||||||
@@ -35,6 +39,72 @@ const TOOL_ENDPOINT_MAP: Record<string, string> = {
|
|||||||
'help': '/api/instructions'
|
'help': '/api/instructions'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed parameter schemas for each tool
|
||||||
|
*/
|
||||||
|
const TOOL_SCHEMAS: Record<string, any> = {
|
||||||
|
search: {
|
||||||
|
query: { type: 'string', description: 'Full-text search query' },
|
||||||
|
type: { type: 'string', description: 'Filter by type: tool_use, tool_result, prompt, summary' },
|
||||||
|
obs_type: { type: 'string', description: 'Observation type filter' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' },
|
||||||
|
dateStart: { type: ['string', 'number'], description: 'Start date (ISO or timestamp)' },
|
||||||
|
dateEnd: { type: ['string', 'number'], description: 'End date (ISO or timestamp)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 10)' },
|
||||||
|
offset: { type: 'number', description: 'Result offset for pagination' },
|
||||||
|
orderBy: { type: 'string', description: 'Sort order: created_at, relevance' }
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
query: { type: 'string', description: 'Search query to find anchor point' },
|
||||||
|
anchor: { type: 'number', description: 'Observation ID as timeline center' },
|
||||||
|
depth_before: { type: 'number', description: 'Observations before anchor (default: 5)' },
|
||||||
|
depth_after: { type: 'number', description: 'Observations after anchor (default: 5)' },
|
||||||
|
type: { type: 'string', description: 'Filter by type' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' }
|
||||||
|
},
|
||||||
|
get_recent_context: {
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||||
|
type: { type: 'string', description: 'Filter by type' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' },
|
||||||
|
dateStart: { type: ['string', 'number'], description: 'Start date' },
|
||||||
|
dateEnd: { type: ['string', 'number'], description: 'End date' }
|
||||||
|
},
|
||||||
|
get_context_timeline: {
|
||||||
|
anchor: { type: 'number', description: 'Observation ID (required)', required: true },
|
||||||
|
depth_before: { type: 'number', description: 'Observations before anchor' },
|
||||||
|
depth_after: { type: 'number', description: 'Observations after anchor' },
|
||||||
|
type: { type: 'string', description: 'Filter by type' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' }
|
||||||
|
},
|
||||||
|
get_observations: {
|
||||||
|
ids: { type: 'array', items: { type: 'number' }, description: 'Array of observation IDs (required)', required: true },
|
||||||
|
orderBy: { type: 'string', description: 'Sort order' },
|
||||||
|
limit: { type: 'number', description: 'Max results' },
|
||||||
|
project: { type: 'string', description: 'Project filter' }
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
operation: { type: 'string', description: 'Operation type: "observations", "timeline", "sessions", etc.' },
|
||||||
|
topic: { type: 'string', description: 'Specific topic for help' }
|
||||||
|
},
|
||||||
|
get_observation: {
|
||||||
|
id: { type: 'number', description: 'Observation ID (required)', required: true }
|
||||||
|
},
|
||||||
|
get_session: {
|
||||||
|
id: { type: 'number', description: 'Session ID (required)', required: true }
|
||||||
|
},
|
||||||
|
get_prompt: {
|
||||||
|
id: { type: 'number', description: 'Prompt ID (required)', required: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call Worker HTTP API endpoint
|
* Call Worker HTTP API endpoint
|
||||||
*/
|
*/
|
||||||
@@ -182,25 +252,47 @@ async function verifyWorkerConnection(): Promise<boolean> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool definitions with HTTP-based handlers
|
* Tool definitions with HTTP-based handlers
|
||||||
* Descriptions removed - use progressive_description tool for parameter documentation
|
* Minimal descriptions - use help() tool with operation parameter for detailed docs
|
||||||
*/
|
*/
|
||||||
const tools = [
|
const tools = [
|
||||||
|
{
|
||||||
|
name: 'get_schema',
|
||||||
|
description: 'Get parameter schema for a tool. Call get_schema(tool_name) for details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { tool_name: { type: 'string' } },
|
||||||
|
required: ['tool_name']
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
// Validate tool_name to prevent prototype pollution
|
||||||
|
const toolName = args.tool_name;
|
||||||
|
if (typeof toolName !== 'string' || !Object.hasOwn(TOOL_SCHEMAS, toolName)) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Unknown tool: ${toolName}\n\nAvailable tools: ${Object.keys(TOOL_SCHEMAS).join(', ')}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = TOOL_SCHEMAS[toolName];
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `# ${toolName} Parameters\n\n${JSON.stringify(schema, null, 2)}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'search',
|
name: 'search',
|
||||||
description: 'Search memory',
|
description: 'Search memory. All parameters optional - call get_schema("search") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
query: z.string().optional(),
|
type: 'object',
|
||||||
type: z.enum(['observations', 'sessions', 'prompts']).optional(),
|
properties: {},
|
||||||
obs_type: z.string().optional(),
|
additionalProperties: true
|
||||||
concepts: z.string().optional(),
|
},
|
||||||
files: z.string().optional(),
|
|
||||||
project: z.string().optional(),
|
|
||||||
dateStart: z.union([z.string(), z.number()]).optional(),
|
|
||||||
dateEnd: z.union([z.string(), z.number()]).optional(),
|
|
||||||
limit: z.number().min(1).max(100).default(20),
|
|
||||||
offset: z.number().min(0).default(0),
|
|
||||||
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc')
|
|
||||||
}),
|
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['search'];
|
const endpoint = TOOL_ENDPOINT_MAP['search'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -208,17 +300,12 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeline',
|
name: 'timeline',
|
||||||
description: 'Timeline context',
|
description: 'Timeline context. All parameters optional - call get_schema("timeline") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
query: z.string().optional(),
|
type: 'object',
|
||||||
anchor: z.number().optional(),
|
properties: {},
|
||||||
depth_before: z.number().min(0).max(100).default(10),
|
additionalProperties: true
|
||||||
depth_after: z.number().min(0).max(100).default(10),
|
},
|
||||||
type: z.string().optional(),
|
|
||||||
concepts: z.string().optional(),
|
|
||||||
files: z.string().optional(),
|
|
||||||
project: z.string().optional()
|
|
||||||
}),
|
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['timeline'];
|
const endpoint = TOOL_ENDPOINT_MAP['timeline'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -226,16 +313,12 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_recent_context',
|
name: 'get_recent_context',
|
||||||
description: 'Recent context',
|
description: 'Recent context. All parameters optional - call get_schema("get_recent_context") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
limit: z.number().min(1).max(100).default(30),
|
type: 'object',
|
||||||
type: z.string().optional(),
|
properties: {},
|
||||||
concepts: z.string().optional(),
|
additionalProperties: true
|
||||||
files: z.string().optional(),
|
},
|
||||||
project: z.string().optional(),
|
|
||||||
dateStart: z.union([z.string(), z.number()]).optional(),
|
|
||||||
dateEnd: z.union([z.string(), z.number()]).optional()
|
|
||||||
}),
|
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['get_recent_context'];
|
const endpoint = TOOL_ENDPOINT_MAP['get_recent_context'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -243,16 +326,18 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_context_timeline',
|
name: 'get_context_timeline',
|
||||||
description: 'Timeline around ID',
|
description: 'Timeline around observation ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
anchor: z.number(),
|
type: 'object',
|
||||||
depth_before: z.number().min(0).max(100).default(10),
|
properties: {
|
||||||
depth_after: z.number().min(0).max(100).default(10),
|
anchor: {
|
||||||
type: z.string().optional(),
|
type: 'number',
|
||||||
concepts: z.string().optional(),
|
description: 'Observation ID (required). Optional params: get_schema("get_context_timeline")'
|
||||||
files: z.string().optional(),
|
}
|
||||||
project: z.string().optional()
|
},
|
||||||
}),
|
required: ['anchor'],
|
||||||
|
additionalProperties: true
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['get_context_timeline'];
|
const endpoint = TOOL_ENDPOINT_MAP['get_context_timeline'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -260,10 +345,12 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'help',
|
name: 'help',
|
||||||
description: 'Usage help',
|
description: 'Get detailed docs. All parameters optional - call get_schema("help") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
topic: z.enum(['workflow', 'search_params', 'examples', 'all']).default('all')
|
type: 'object',
|
||||||
}),
|
properties: {},
|
||||||
|
additionalProperties: true
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['help'];
|
const endpoint = TOOL_ENDPOINT_MAP['help'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -271,43 +358,70 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_observation',
|
name: 'get_observation',
|
||||||
description: 'Fetch by ID',
|
description: 'Fetch observation by ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
id: z.number()
|
type: 'object',
|
||||||
}),
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Observation ID (required)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIWithPath('/api/observation', args.id);
|
return await callWorkerAPIWithPath('/api/observation', args.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_observations',
|
name: 'get_observations',
|
||||||
description: 'Batch fetch',
|
description: 'Batch fetch observations',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
ids: z.array(z.number()),
|
type: 'object',
|
||||||
orderBy: z.enum(['date_desc', 'date_asc']).optional(),
|
properties: {
|
||||||
limit: z.number().optional(),
|
ids: {
|
||||||
project: z.string().optional()
|
type: 'array',
|
||||||
}),
|
items: { type: 'number' },
|
||||||
|
description: 'Array of observation IDs (required). Optional params: get_schema("get_observations")'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['ids'],
|
||||||
|
additionalProperties: true
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIPost('/api/observations/batch', args);
|
return await callWorkerAPIPost('/api/observations/batch', args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_session',
|
name: 'get_session',
|
||||||
description: 'Session by ID',
|
description: 'Fetch session by ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
id: z.number()
|
type: 'object',
|
||||||
}),
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Session ID (required)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIWithPath('/api/session', args.id);
|
return await callWorkerAPIWithPath('/api/session', args.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_prompt',
|
name: 'get_prompt',
|
||||||
description: 'Prompt by ID',
|
description: 'Fetch prompt by ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
id: z.number()
|
type: 'object',
|
||||||
}),
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Prompt ID (required)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIWithPath('/api/prompt', args.id);
|
return await callWorkerAPIWithPath('/api/prompt', args.id);
|
||||||
}
|
}
|
||||||
@@ -333,7 +447,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||||||
tools: tools.map(tool => ({
|
tools: tools.map(tool => ({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: zodToJsonSchema(tool.inputSchema) as Record<string, unknown>
|
inputSchema: tool.inputSchema
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -382,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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
toRelativePath,
|
toRelativePath,
|
||||||
extractFirstFile
|
extractFirstFile
|
||||||
} from '../shared/timeline-formatting.js';
|
} from '../shared/timeline-formatting.js';
|
||||||
|
import { getProjectName } from '../utils/project-name.js';
|
||||||
|
|
||||||
// Version marker path - use homedir-based path that works in both CJS and ESM contexts
|
// Version marker path - use homedir-based path that works in both CJS and ESM contexts
|
||||||
const VERSION_MARKER_PATH = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', '.install-version');
|
const VERSION_MARKER_PATH = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', '.install-version');
|
||||||
@@ -222,7 +223,7 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
|
|||||||
export async function generateContext(input?: ContextInput, useColors: boolean = false): Promise<string> {
|
export async function generateContext(input?: ContextInput, useColors: boolean = false): Promise<string> {
|
||||||
const config = loadContextConfig();
|
const config = loadContextConfig();
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
const project = getProjectName(cwd);
|
||||||
|
|
||||||
let db: SessionStore | null = null;
|
let db: SessionStore | null = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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');
|
||||||
@@ -16,6 +17,7 @@ const HEALTH_CHECK_TIMEOUT_MS = 10000;
|
|||||||
const HEALTH_CHECK_INTERVAL_MS = 200;
|
const HEALTH_CHECK_INTERVAL_MS = 200;
|
||||||
const HEALTH_CHECK_FETCH_TIMEOUT_MS = 1000;
|
const HEALTH_CHECK_FETCH_TIMEOUT_MS = 1000;
|
||||||
const PROCESS_EXIT_CHECK_INTERVAL_MS = 100;
|
const PROCESS_EXIT_CHECK_INTERVAL_MS = 100;
|
||||||
|
const HTTP_SHUTDOWN_TIMEOUT_MS = 2000;
|
||||||
|
|
||||||
interface PidInfo {
|
interface PidInfo {
|
||||||
pid: number;
|
pid: number;
|
||||||
@@ -99,8 +101,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',
|
||||||
@@ -171,34 +174,65 @@ export class ProcessManager {
|
|||||||
|
|
||||||
static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise<boolean> {
|
static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): 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 +263,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(HTTP_SHUTDOWN_TIMEOUT_MS)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for worker to actually stop responding
|
||||||
|
return await this.waitForWorkerDown(port, PROCESS_STOP_TIMEOUT_MS);
|
||||||
|
} 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, PROCESS_EXIT_CHECK_INTERVAL_MS));
|
||||||
|
} 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 {
|
||||||
@@ -271,29 +365,39 @@ 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 = HEALTH_CHECK_TIMEOUT_MS): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
// Increase timeout on Windows to account for slower process startup
|
||||||
|
const adjustedTimeout = isWindows ? timeoutMs * 2 : timeoutMs;
|
||||||
|
|
||||||
while (Date.now() - startTime < timeoutMs) {
|
while (Date.now() - startTime < adjustedTimeout) {
|
||||||
// Check if process is still alive
|
// Check if process is still alive
|
||||||
if (!this.isProcessAlive(pid)) {
|
if (!this.isProcessAlive(pid)) {
|
||||||
return { success: false, error: 'Process died during startup' };
|
const errorMsg = isWindows
|
||||||
|
? `Process died during startup\n\nTroubleshooting:\n1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes\n2. Verify port ${port} is not in use: netstat -ano | findstr ${port}\n3. Check worker logs in ~/.claude-mem/logs/\n4. See GitHub issues: #363, #367, #371, #373\n5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`
|
||||||
|
: 'Process died during startup';
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try health check
|
// Try readiness check (changed from /health to /api/readiness)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||||
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return { success: true, pid };
|
return { success: true, pid };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not ready yet
|
// Not ready yet, continue polling
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Health check timed out' };
|
const timeoutMsg = isWindows
|
||||||
|
? `Worker failed to start on Windows (readiness check timed out after ${adjustedTimeout}ms)\n\nTroubleshooting:\n1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes\n2. Verify port ${port} is not in use: netstat -ano | findstr ${port}\n3. Check worker logs in ~/.claude-mem/logs/\n4. See GitHub issues: #363, #367, #371, #373\n5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`
|
||||||
|
: `Readiness check timed out after ${adjustedTimeout}ms`;
|
||||||
|
|
||||||
|
return { success: false, error: timeoutMsg };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async waitForExit(pid: number, timeout: number): Promise<void> {
|
private static async waitForExit(pid: number, timeout: number): Promise<void> {
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export class ChromaSync {
|
|||||||
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
||||||
this.transport = new StdioClientTransport({
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
const transportOptions: any = {
|
||||||
command: 'uvx',
|
command: 'uvx',
|
||||||
args: [
|
args: [
|
||||||
'--python', pythonVersion,
|
'--python', pythonVersion,
|
||||||
@@ -110,7 +112,16 @@ export class ChromaSync {
|
|||||||
'--data-dir', this.VECTOR_DB_DIR
|
'--data-dir', this.VECTOR_DB_DIR
|
||||||
],
|
],
|
||||||
stderr: 'ignore'
|
stderr: 'ignore'
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// CRITICAL: On Windows, try to hide console window to prevent PowerShell popups
|
||||||
|
// Note: windowsHide may not be supported by MCP SDK's StdioClientTransport
|
||||||
|
if (isWindows) {
|
||||||
|
transportOptions.windowsHide = true;
|
||||||
|
logger.debug('CHROMA_SYNC', 'Windows detected, attempting to hide console window', { project: this.project });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transport = new StdioClientTransport(transportOptions);
|
||||||
|
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
name: 'claude-mem-chroma-sync',
|
name: 'claude-mem-chroma-sync',
|
||||||
|
|||||||
+125
-34
@@ -14,7 +14,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { exec } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -32,7 +32,7 @@ import { TimelineService } from './worker/TimelineService.js';
|
|||||||
import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js';
|
import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js';
|
||||||
|
|
||||||
// Import HTTP layer
|
// Import HTTP layer
|
||||||
import { createMiddleware, summarizeRequestBody as summarizeBody } from './worker/http/middleware.js';
|
import { createMiddleware, summarizeRequestBody as summarizeBody, requireLocalhost } from './worker/http/middleware.js';
|
||||||
import { ViewerRoutes } from './worker/http/routes/ViewerRoutes.js';
|
import { ViewerRoutes } from './worker/http/routes/ViewerRoutes.js';
|
||||||
import { SessionRoutes } from './worker/http/routes/SessionRoutes.js';
|
import { SessionRoutes } from './worker/http/routes/SessionRoutes.js';
|
||||||
import { DataRoutes } from './worker/http/routes/DataRoutes.js';
|
import { DataRoutes } from './worker/http/routes/DataRoutes.js';
|
||||||
@@ -45,6 +45,10 @@ export class WorkerService {
|
|||||||
private startTime: number = Date.now();
|
private startTime: number = Date.now();
|
||||||
private mcpClient: Client;
|
private mcpClient: Client;
|
||||||
|
|
||||||
|
// Initialization flags for MCP/SDK readiness tracking
|
||||||
|
private mcpReady: boolean = false;
|
||||||
|
private initializationCompleteFlag: boolean = false;
|
||||||
|
|
||||||
// Domain services
|
// Domain services
|
||||||
private dbManager: DatabaseManager;
|
private dbManager: DatabaseManager;
|
||||||
private sessionManager: SessionManager;
|
private sessionManager: SessionManager;
|
||||||
@@ -128,17 +132,36 @@ export class WorkerService {
|
|||||||
hasIpc: typeof process.send === 'function',
|
hasIpc: typeof process.send === 'function',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
|
initialized: this.initializationCompleteFlag,
|
||||||
|
mcpReady: this.mcpReady,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Readiness check endpoint - returns 503 until full initialization completes
|
||||||
|
// Used by ProcessManager and worker-utils to ensure worker is fully ready before routing requests
|
||||||
|
this.app.get('/api/readiness', (_req, res) => {
|
||||||
|
if (this.initializationCompleteFlag) {
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'ready',
|
||||||
|
mcpReady: this.mcpReady,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(503).json({
|
||||||
|
status: 'initializing',
|
||||||
|
message: 'Worker is still initializing, please retry',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Version endpoint - returns the worker's current version
|
// Version endpoint - returns the worker's current version
|
||||||
this.app.get('/api/version', (_req, res) => {
|
this.app.get('/api/version', (_req, res) => {
|
||||||
|
const { homedir } = require('os');
|
||||||
|
const { readFileSync } = require('fs');
|
||||||
|
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||||
|
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read version from marketplace package.json
|
// Read version from marketplace package.json
|
||||||
const { homedir } = require('os');
|
|
||||||
const { readFileSync } = require('fs');
|
|
||||||
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
|
||||||
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
|
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||||
res.status(200).json({ version: packageJson.version });
|
res.status(200).json({ version: packageJson.version });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -155,26 +178,35 @@ export class WorkerService {
|
|||||||
// Instructions endpoint - loads SKILL.md sections on-demand for progressive instruction loading
|
// Instructions endpoint - loads SKILL.md sections on-demand for progressive instruction loading
|
||||||
this.app.get('/api/instructions', async (req, res) => {
|
this.app.get('/api/instructions', async (req, res) => {
|
||||||
const topic = (req.query.topic as string) || 'all';
|
const topic = (req.query.topic as string) || 'all';
|
||||||
// Read SKILL.md from plugin directory
|
const operation = req.query.operation as string | undefined;
|
||||||
|
|
||||||
// Path resolution: __dirname is build output directory (plugin/scripts/)
|
// Path resolution: __dirname is build output directory (plugin/scripts/)
|
||||||
// SKILL.md is at plugin/skills/mem-search/SKILL.md
|
// SKILL.md is at plugin/skills/mem-search/SKILL.md
|
||||||
const skillPath = path.join(__dirname, '../skills/mem-search/SKILL.md');
|
// Operations are at plugin/skills/mem-search/operations/*.md
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
|
let content: string;
|
||||||
|
|
||||||
// Extract section based on topic
|
if (operation) {
|
||||||
const section = this.extractInstructionSection(fullContent, topic);
|
// Load specific operation file
|
||||||
|
const operationPath = path.join(__dirname, '../skills/mem-search/operations', `${operation}.md`);
|
||||||
|
content = await fs.promises.readFile(operationPath, 'utf-8');
|
||||||
|
} else {
|
||||||
|
// Load SKILL.md and extract section based on topic (backward compatibility)
|
||||||
|
const skillPath = path.join(__dirname, '../skills/mem-search/SKILL.md');
|
||||||
|
const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
|
||||||
|
content = this.extractInstructionSection(fullContent, topic);
|
||||||
|
}
|
||||||
|
|
||||||
// Return in MCP format
|
// Return in MCP format
|
||||||
res.json({
|
res.json({
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: section
|
text: content
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('WORKER', 'Failed to load instructions', { topic, skillPath }, error as Error);
|
logger.error('WORKER', 'Failed to load instructions', { topic, operation }, error as Error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -185,8 +217,8 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin endpoints for process management
|
// Admin endpoints for process management (localhost-only)
|
||||||
this.app.post('/api/admin/restart', async (_req, res) => {
|
this.app.post('/api/admin/restart', requireLocalhost, async (_req, res) => {
|
||||||
res.json({ status: 'restarting' });
|
res.json({ status: 'restarting' });
|
||||||
|
|
||||||
// On Windows, if managed by wrapper, send message to parent to handle restart
|
// On Windows, if managed by wrapper, send message to parent to handle restart
|
||||||
@@ -207,7 +239,7 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.post('/api/admin/shutdown', async (_req, res) => {
|
this.app.post('/api/admin/shutdown', requireLocalhost, async (_req, res) => {
|
||||||
res.json({ status: 'shutting_down' });
|
res.json({ status: 'shutting_down' });
|
||||||
|
|
||||||
// On Windows, if managed by wrapper, send message to parent to handle shutdown
|
// On Windows, if managed by wrapper, send message to parent to handle shutdown
|
||||||
@@ -295,25 +327,47 @@ export class WorkerService {
|
|||||||
*/
|
*/
|
||||||
private async cleanupOrphanedProcesses(): Promise<void> {
|
private async cleanupOrphanedProcesses(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Find all chroma-mcp processes
|
const isWindows = process.platform === 'win32';
|
||||||
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
|
|
||||||
|
|
||||||
if (!stdout.trim()) {
|
|
||||||
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = stdout.trim().split('\n');
|
|
||||||
const pids: number[] = [];
|
const pids: number[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
if (isWindows) {
|
||||||
const parts = line.trim().split(/\s+/);
|
// Windows: Use PowerShell Get-CimInstance to find chroma-mcp processes
|
||||||
if (parts.length > 1) {
|
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`;
|
||||||
const pid = parseInt(parts[1], 10);
|
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||||
if (!isNaN(pid)) {
|
|
||||||
|
if (!stdout.trim()) {
|
||||||
|
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Windows)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pidStrings = stdout.trim().split('\n');
|
||||||
|
for (const pidStr of pidStrings) {
|
||||||
|
const pid = parseInt(pidStr.trim(), 10);
|
||||||
|
// SECURITY: Validate PID is positive integer before adding to list
|
||||||
|
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
|
||||||
pids.push(pid);
|
pids.push(pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Unix: Use ps aux | grep
|
||||||
|
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
|
||||||
|
|
||||||
|
if (!stdout.trim()) {
|
||||||
|
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Unix)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = stdout.trim().split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const pid = parseInt(parts[1], 10);
|
||||||
|
// SECURITY: Validate PID is positive integer before adding to list
|
||||||
|
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
|
||||||
|
pids.push(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pids.length === 0) {
|
if (pids.length === 0) {
|
||||||
@@ -321,12 +375,28 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
||||||
|
platform: isWindows ? 'Windows' : 'Unix',
|
||||||
count: pids.length,
|
count: pids.length,
|
||||||
pids
|
pids
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kill all found processes
|
// Kill all found processes
|
||||||
await execAsync(`kill ${pids.join(' ')}`);
|
if (isWindows) {
|
||||||
|
for (const pid of pids) {
|
||||||
|
// SECURITY: Double-check PID validation before using in taskkill command
|
||||||
|
if (!Number.isInteger(pid) || pid <= 0) {
|
||||||
|
logger.warn('SYSTEM', 'Skipping invalid PID', { pid });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000, stdio: 'ignore' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('SYSTEM', 'Failed to kill orphaned process', { pid }, error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await execAsync(`kill ${pids.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
|
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -380,7 +450,7 @@ export class WorkerService {
|
|||||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||||
|
|
||||||
// Connect to MCP server
|
// Connect to MCP server with timeout guard
|
||||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: 'node',
|
command: 'node',
|
||||||
@@ -388,10 +458,19 @@ export class WorkerService {
|
|||||||
env: process.env
|
env: process.env
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.mcpClient.connect(transport);
|
// Add timeout guard to prevent hanging on MCP connection (15 seconds)
|
||||||
|
const MCP_INIT_TIMEOUT_MS = 15000;
|
||||||
|
const mcpConnectionPromise = this.mcpClient.connect(transport);
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('MCP connection timeout after 15s')), MCP_INIT_TIMEOUT_MS)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.race([mcpConnectionPromise, timeoutPromise]);
|
||||||
|
this.mcpReady = true;
|
||||||
logger.success('WORKER', 'Connected to MCP server');
|
logger.success('WORKER', 'Connected to MCP server');
|
||||||
|
|
||||||
// Signal that initialization is complete
|
// Signal that initialization is complete
|
||||||
|
this.initializationCompleteFlag = true;
|
||||||
this.resolveInitialization();
|
this.resolveInitialization();
|
||||||
logger.info('SYSTEM', 'Background initialization complete');
|
logger.info('SYSTEM', 'Background initialization complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -492,6 +571,12 @@ export class WorkerService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SECURITY: Validate PID is a positive integer to prevent command injection
|
||||||
|
if (!Number.isInteger(parentPid) || parentPid <= 0) {
|
||||||
|
logger.warn('SYSTEM', 'Invalid parent PID for child process enumeration', { parentPid });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
||||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||||
@@ -499,7 +584,7 @@ export class WorkerService {
|
|||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(s => parseInt(s.trim(), 10))
|
.map(s => parseInt(s.trim(), 10))
|
||||||
.filter(n => !isNaN(n));
|
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0); // SECURITY: Validate each PID
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
||||||
return [];
|
return [];
|
||||||
@@ -510,6 +595,12 @@ export class WorkerService {
|
|||||||
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
||||||
*/
|
*/
|
||||||
private async forceKillProcess(pid: number): Promise<void> {
|
private async forceKillProcess(pid: number): Promise<void> {
|
||||||
|
// SECURITY: Validate PID is a positive integer to prevent command injection
|
||||||
|
if (!Number.isInteger(pid) || pid <= 0) {
|
||||||
|
logger.warn('SYSTEM', 'Invalid PID for force kill', { pid });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// /T kills entire process tree, /F forces termination
|
// /T kills entire process tree, /F forces termination
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,34 @@ export function createMiddleware(
|
|||||||
return middlewares;
|
return middlewares;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to require localhost-only access
|
||||||
|
* Used for admin endpoints that should not be exposed when binding to 0.0.0.0
|
||||||
|
*/
|
||||||
|
export function requireLocalhost(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
const clientIp = req.ip || req.connection.remoteAddress || '';
|
||||||
|
const isLocalhost =
|
||||||
|
clientIp === '127.0.0.1' ||
|
||||||
|
clientIp === '::1' ||
|
||||||
|
clientIp === '::ffff:127.0.0.1' ||
|
||||||
|
clientIp === 'localhost';
|
||||||
|
|
||||||
|
if (!isLocalhost) {
|
||||||
|
logger.warn('SECURITY', 'Admin endpoint access denied - not localhost', {
|
||||||
|
endpoint: req.path,
|
||||||
|
clientIp,
|
||||||
|
method: req.method
|
||||||
|
});
|
||||||
|
res.status(403).json({
|
||||||
|
error: 'Forbidden',
|
||||||
|
message: 'Admin endpoints are only accessible from localhost'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Summarize request body for logging
|
* Summarize request body for logging
|
||||||
* Used to avoid logging sensitive data or large payloads
|
* Used to avoid logging sensitive data or large payloads
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync, existsSync } from 'fs';
|
||||||
import { getPackageRoot } from '../../../../shared/paths.js';
|
import { getPackageRoot } from '../../../../shared/paths.js';
|
||||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
||||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||||
@@ -41,7 +41,19 @@ export class ViewerRoutes extends BaseRouteHandler {
|
|||||||
*/
|
*/
|
||||||
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
|
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
const packageRoot = getPackageRoot();
|
const packageRoot = getPackageRoot();
|
||||||
const viewerPath = path.join(packageRoot, 'plugin', 'ui', 'viewer.html');
|
|
||||||
|
// Try cache structure first (ui/viewer.html), then marketplace structure (plugin/ui/viewer.html)
|
||||||
|
const viewerPaths = [
|
||||||
|
path.join(packageRoot, 'ui', 'viewer.html'),
|
||||||
|
path.join(packageRoot, 'plugin', 'ui', 'viewer.html')
|
||||||
|
];
|
||||||
|
|
||||||
|
const viewerPath = viewerPaths.find(p => existsSync(p));
|
||||||
|
|
||||||
|
if (!viewerPath) {
|
||||||
|
throw new Error('Viewer UI not found at any expected location');
|
||||||
|
}
|
||||||
|
|
||||||
const html = readFileSync(viewerPath, 'utf-8');
|
const html = readFileSync(viewerPath, 'utf-8');
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.send(html);
|
res.send(html);
|
||||||
|
|||||||
@@ -58,17 +58,18 @@ export function getWorkerHost(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if worker is responsive by trying the health endpoint
|
* Check if worker is responsive and fully initialized by trying the readiness endpoint
|
||||||
|
* Changed from /health to /api/readiness to ensure MCP initialization is complete
|
||||||
*/
|
*/
|
||||||
async function isWorkerHealthy(): Promise<boolean> {
|
async function isWorkerHealthy(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||||
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug('SYSTEM', 'Worker health check failed', {
|
logger.debug('SYSTEM', 'Worker readiness check failed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
errorType: error?.constructor?.name
|
errorType: error?.constructor?.name
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract project name from working directory path
|
||||||
|
* Handles edge cases: null/undefined cwd, drive roots, trailing slashes
|
||||||
|
*
|
||||||
|
* @param cwd - Current working directory (absolute path)
|
||||||
|
* @returns Project name or "unknown-project" if extraction fails
|
||||||
|
*/
|
||||||
|
export function getProjectName(cwd: string | null | undefined): string {
|
||||||
|
if (!cwd || cwd.trim() === '') {
|
||||||
|
logger.warn('PROJECT_NAME', 'Empty cwd provided, using fallback', { cwd });
|
||||||
|
return 'unknown-project';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract basename (handles trailing slashes automatically)
|
||||||
|
const basename = path.basename(cwd);
|
||||||
|
|
||||||
|
// Edge case: Drive roots on Windows (C:\, J:\) or Unix root (/)
|
||||||
|
// path.basename('C:\') returns '' (empty string)
|
||||||
|
if (basename === '') {
|
||||||
|
// Extract drive letter on Windows, or use 'root' on Unix
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
if (isWindows && cwd.match(/^[A-Z]:\\/i)) {
|
||||||
|
const driveLetter = cwd[0].toUpperCase();
|
||||||
|
const projectName = `drive-${driveLetter}`;
|
||||||
|
logger.info('PROJECT_NAME', 'Drive root detected', { cwd, projectName });
|
||||||
|
return projectName;
|
||||||
|
} else {
|
||||||
|
logger.warn('PROJECT_NAME', 'Root directory detected, using fallback', { cwd });
|
||||||
|
return 'unknown-project';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename;
|
||||||
|
}
|
||||||
@@ -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