Compare commits

...

20 Commits

Author SHA1 Message Date
Alex Newman 0768fafd83 chore: bump version to 7.4.2
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 17:19:17 -05:00
Alex Newman 5ce656037e Refactor worker commands from npm scripts to claude-mem CLI
- Updated all instances of `npm run worker:restart` to `claude-mem restart` in documentation and code comments for consistency.
- Modified error messages and logging to reflect the new command structure.
- Adjusted worker management commands in various troubleshooting documents.
- Changed the worker status check message to guide users towards the new command.
2025-12-20 17:16:20 -05:00
Alex Newman e27f8e4963 added path alias script 2025-12-20 17:03:52 -05:00
ToxMox af145cfaef fix(windows): improve worker stop/restart reliability (#395)
* fix(windows): enable worker logging on Windows

Previously, Windows worker startup via PowerShell Start-Process did not
redirect stdout/stderr to log files, making debugging startup failures
impossible. This adds -RedirectStandardOutput and -RedirectStandardError
to capture worker logs to ~/.claude-mem/logs/worker-YYYY-MM-DD.log.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(windows): improve worker stop/restart reliability

- Use HTTP shutdown endpoint as primary stop method (worker kills itself)
- Only remove PID file after confirming worker is actually dead
- Remove auto-respawn from wrapper to prevent PID file mismatches
- Wrapper now exits when inner worker crashes (hooks will restart)

This hopefully fixes issues where npm run worker:stop would fail silently when
the worker was started from hooks, leaving zombie processes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 16:50:32 -05:00
Alex Newman 15fe0cfe3c docs: simplify build commands section in CLAUDE.md 2025-12-19 18:32:24 -05:00
Alex Newman c0ed9bbcfd chore: update CHANGELOG.md 2025-12-19 15:12:42 -05:00
Alex Newman 8040c6d559 fix: redirect MCP server logs to stderr to preserve JSON-RPC protocol
MCP uses stdio transport where stdout is reserved for JSON-RPC messages.
Console.log was writing startup logs to stdout, causing Claude Desktop
to parse "[2025-12-19..." as a JSON array and fail.

Fixes #396

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 15:11:30 -05:00
Alex Newman 7187220b24 Redirect console.log to stderr in mcp-server.ts to prevent JSON-RPC protocol interference; update mem-search.zip 2025-12-19 15:10:32 -05:00
Alex Newman ca52950b2a chore: update CHANGELOG.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 22:51:42 -05:00
Alex Newman ee1441f462 chore: bump version to 7.4.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 22:50:46 -05:00
Alex Newman c4af31f48d Optimize MCP tool token usage with schema reference pattern
Reduces MCP tool token consumption by ~90% through progressive disclosure. Tools now show minimal schemas with get_schema() for details on demand.
2025-12-17 22:47:30 -05:00
Alex Newman c2742d5664 chore: bump plugin version to 7.3.9 2025-12-17 19:52:03 -05:00
Alex Newman 0c45919261 chore: update CHANGELOG.md 2025-12-17 19:41:30 -05:00
Alex Newman a3ab898e04 chore: bump version to 7.3.9 2025-12-17 19:40:46 -05:00
Alex Newman dea67c0d86 fix: MCP server compatibility and web UI path resolution
Fixes #371, #369

**Issue #371: MCP server fails when Bun not in PATH**
- Changed MCP server shebang from `#!/usr/bin/env bun` to `#!/usr/bin/env node`
- MCP server now works regardless of whether Bun is in PATH
- Worker service correctly uses getBunPath() to find Bun in common install locations

**Issue #369: Web UI returns ENOENT error**
- Fixed hardcoded 'plugin/' path in ViewerRoutes
- Now checks both cache structure (ui/viewer.html) and marketplace structure (plugin/ui/viewer.html)
- Web UI now works from both ~/.claude/plugins/cache and ~/.claude/plugins/marketplaces

**Technical Details:**
- Updated build-hooks.js to use Node shebang for MCP server (line 169)
- Enhanced ViewerRoutes.handleViewerUI() to try multiple path patterns
- Added existsSync check to find viewer.html in either location

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 19:36:03 -05:00
Alex Newman d13a2c237c Update mem-search plugin with new features and improvements 2025-12-17 19:26:44 -05:00
Alex Newman c592f0aa69 chore: update CHANGELOG.md 2025-12-17 19:26:07 -05:00
Alex Newman 85a2472e4e chore: bump version to 7.3.8 2025-12-17 19:25:11 -05:00
Alex Newman 0cb3256b2d fix(security): add localhost-only protection for admin endpoints
Adds middleware to restrict /api/admin/restart and /api/admin/shutdown
to localhost-only access. This prevents DoS attacks when the worker
service is bound to 0.0.0.0 for remote UI access.

Implementation:
- Created requireLocalhost middleware in middleware.ts
- Applied to both admin endpoints
- Checks client IP against localhost addresses (127.0.0.1, ::1, etc.)
- Returns 403 Forbidden for non-localhost requests

Addresses security concern raised in PR #368 with cleaner DRY approach.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 19:06:33 -05:00
Alex Newman 44029862b1 chore: update CHANGELOG.md 2025-12-17 18:48:20 -05:00
48 changed files with 1028 additions and 474 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "7.3.7",
"version": "7.4.2",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+1
View File
@@ -1,3 +1,4 @@
datasets/
node_modules/
dist/
*.log
+74
View File
@@ -4,6 +4,80 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [7.4.1] - 2025-12-19
## Bug Fixes
- **MCP Server**: Redirect logs to stderr to preserve JSON-RPC protocol (#396)
- MCP uses stdio transport where stdout is reserved for JSON-RPC messages
- Console.log was writing startup logs to stdout, causing Claude Desktop to parse log lines as JSON and fail
## [7.4.0] - 2025-12-18
## What's New
### MCP Tool Token Reduction
Optimized MCP tool definitions for reduced token consumption in Claude Code sessions through progressive parameter disclosure.
**Changes:**
- Streamlined MCP tool schemas with minimal inline definitions
- Added `get_schema()` tool for on-demand parameter documentation
- Enhanced worker API with operation-based instruction loading
This release improves session efficiency by reducing the token overhead of MCP tool definitions while maintaining full functionality through progressive disclosure.
## [7.3.9] - 2025-12-18
## Fixes
- Fix MCP server compatibility and web UI path resolution
This patch release addresses compatibility issues with the MCP server and resolves path resolution problems in the web UI.
## [7.3.8] - 2025-12-18
## Security Fix
Added localhost-only protection for admin endpoints to prevent DoS attacks when worker service is bound to 0.0.0.0 for remote UI access.
### Changes
- Created `requireLocalhost` middleware to restrict admin endpoints
- Applied to `/api/admin/restart` and `/api/admin/shutdown`
- Returns 403 Forbidden for non-localhost requests
### Security Impact
Prevents unauthorized shutdown/restart of worker service when exposed on network.
Fixes security concern raised in #368.
## [7.3.7] - 2025-12-17
## Windows Platform Stabilization
This patch release includes comprehensive improvements for Windows platform stability and reliability.
### Key Improvements
- **Worker Readiness Tracking**: Added `/api/readiness` endpoint with MCP/SDK initialization flags to prevent premature connection attempts
- **Process Tree Cleanup**: Implemented recursive process enumeration on Windows to prevent zombie socket processes
- **Bun Runtime Migration**: Migrated worker wrapper from Node.js to Bun for consistency and reliability
- **Centralized Project Name Utility**: Consolidated duplicate project name extraction logic with Windows drive root handling
- **Enhanced Error Messages**: Added platform-aware logging and detailed Windows troubleshooting guidance
- **Subprocess Console Hiding**: Standardized `windowsHide: true` across all child process spawns to prevent console window flashing
### Technical Details
- Worker service tracks MCP and SDK readiness states separately
- ChromaSync service properly tracks subprocess PIDs for Windows cleanup
- Worker wrapper uses Bun runtime with enhanced socket cleanup via process tree enumeration
- Increased timeouts on Windows platform (30s worker startup, 10s hook timeouts)
- Logger utility includes platform and PID information for better debugging
This represents a major reliability improvement for Windows users, eliminating common issues with worker startup failures, orphaned processes, and zombie sockets.
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.6...v7.3.7
## [7.3.6] - 2025-12-17
## Bug Fixes
+1 -6
View File
@@ -33,12 +33,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
## Build Commands
```bash
npm run build-and-sync # Build, sync to marketplace, restart worker (most common)
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
npm run build-and-sync # Build, sync to marketplace, restart worker
```
**Viewer UI**: http://localhost:37777
+1 -1
View File
@@ -396,7 +396,7 @@ If you're experiencing issues, describe the problem to Claude and the troublesho
**Common Issues:**
- Worker not starting → `npm run worker:restart`
- Worker not starting → `claude-mem restart`
- No context appearing → `npm run test:context`
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
- Search not working → Check FTS5 tables exist
@@ -106,7 +106,7 @@ pm2 logs claude-mem-worker # View logs
```bash
npm run worker:start # Start 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:logs # View logs
```
@@ -305,7 +305,7 @@ No migration logic runs on subsequent sessions.
| `pm2 list` | `npm run worker:status` | Shows worker status |
| `pm2 start <script>` | `npm run worker:start` | Start 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 logs claude-mem-worker` | `npm run worker:logs` | View logs |
| `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
# Restart worker
npm run worker:restart
claude-mem restart
```
### Scenario 2: Stale PID File (Process Dead)
@@ -483,7 +483,7 @@ lsof -i :37777
kill -9 <PID>
# Restart worker
npm run worker:restart
claude-mem restart
```
### Common Error Messages
@@ -416,7 +416,7 @@ If searches fail, check worker service:
```bash
npm run worker:status # Check status
npm run worker:restart # Restart worker
claude-mem restart # Restart worker
npm run worker:logs # View logs
```
+1 -1
View File
@@ -500,7 +500,7 @@ npm run worker:start
npm run worker:stop
# Restart worker
npm run worker:restart
claude-mem restart
# View logs
npm run worker:logs
+5 -5
View File
@@ -316,7 +316,7 @@ Edit `~/.claude-mem/settings.json`:
Then restart the worker:
```bash
npm run worker:restart
claude-mem restart
```
### Custom Model
@@ -331,7 +331,7 @@ Edit `~/.claude-mem/settings.json`:
Then restart the worker:
```bash
export CLAUDE_MEM_MODEL=opus
npm run worker:restart
claude-mem restart
```
### Custom Skip Tools
@@ -388,7 +388,7 @@ Enable debug logging:
```bash
export DEBUG=claude-mem:*
npm run worker:restart
claude-mem restart
npm run worker:logs
```
@@ -406,7 +406,7 @@ npm run worker:logs
1. Restart worker after changes:
```bash
npm run worker:restart
claude-mem restart
```
2. Verify environment variables:
@@ -440,7 +440,7 @@ If port 37777 is already in use:
2. Restart worker:
```bash
npm run worker:restart
claude-mem restart
```
3. Verify new port:
+2 -2
View File
@@ -165,7 +165,7 @@ npm run build
1. Make changes to React components in `src/ui/viewer/`
2. Build: `npm run build`
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
**Hot Reload**: Not currently supported. Full rebuild + restart required for changes.
@@ -456,7 +456,7 @@ export async function createObservation(
```bash
export DEBUG=claude-mem:*
npm run worker:restart
claude-mem restart
npm run worker:logs
```
+2 -2
View File
@@ -94,7 +94,7 @@ git checkout beta/endless-mode
npm install
# Restart the worker
npm run worker:restart
claude-mem restart
```
**To return to stable:**
@@ -103,7 +103,7 @@ npm run worker:restart
cd ~/.claude/plugins/marketplaces/thedotmack/
git checkout main
npm install
npm run worker:restart
claude-mem restart
```
## Summary
+1 -1
View File
@@ -534,7 +534,7 @@ npm run worker:status
npm run worker:logs
# Restart
npm run worker:restart
claude-mem restart
# Stop
npm run worker:stop
+1 -1
View File
@@ -57,7 +57,7 @@ CLAUDE_MEM_PYTHON_VERSION=3.13 # Python version for chroma-mcp
```bash
npm run build # Compile TypeScript (hooks + worker)
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:status # Check worker status
```
+8 -8
View File
@@ -48,14 +48,14 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
4. Restart worker service:
```bash
npm run worker:restart
claude-mem restart
```
5. Check for port conflicts:
```bash
# If port 37777 is in use by another service
export CLAUDE_MEM_WORKER_PORT=38000
npm run worker:restart
claude-mem restart
```
### Theme Toggle Not Persisting
@@ -110,7 +110,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
5. Restart worker and refresh browser:
```bash
npm run worker:restart
claude-mem restart
```
### 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:
```bash
export CLAUDE_MEM_WORKER_PORT=38000
npm run worker:restart
claude-mem restart
```
4. Verify new port:
@@ -282,7 +282,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
4. Restart worker:
```bash
npm run worker:restart
claude-mem restart
```
## Hook Issues
@@ -644,7 +644,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
2. Restart worker:
```bash
npm run worker:restart
claude-mem restart
```
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
export DEBUG=claude-mem:*
npm run worker:restart
claude-mem restart
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.
**Solution**: Restart worker with `npm run worker:restart`.
**Solution**: Restart worker with `claude-mem restart`.
### "Database is locked"
+1 -1
View File
@@ -86,7 +86,7 @@ npm run worker:start
npm run worker:stop
# Restart worker service
npm run worker:restart
claude-mem restart
# View worker logs
npm run worker:logs
+1 -1
View File
@@ -176,7 +176,7 @@ This design ensures that private content never reaches the database, search indi
1. Verify correct syntax: `<private>content</private>`
2. Check `~/.claude-mem/silent.log` for errors
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
+1 -1
View File
@@ -364,7 +364,7 @@ If search isn't working, check the worker service:
```bash
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
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.3.7",
"version": "7.4.2",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.3.7",
"version": "7.4.2",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem-plugin",
"version": "7.3.7",
"version": "7.4.1",
"private": true,
"description": "Runtime dependencies for claude-mem bundled hooks",
"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
+94
View File
@@ -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
*/
@@ -351,6 +442,9 @@ try {
installDeps();
console.error('✅ Dependencies installed');
}
// Step 4: Install CLI to PATH
installCLI();
} catch (e) {
console.error('❌ Installation failed:', e.message);
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
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,2 +1,2 @@
#!/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.
+115
View File
@@ -212,3 +212,118 @@ help(topic="all") # Get complete guide
- ALWAYS get timeline context to understand what was happening
- ALWAYS use `get_observations` when fetching 2+ observations
- 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:
```bash
npm run worker:restart
claude-mem restart
```
```
@@ -44,7 +44,7 @@ npm run worker:status
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm install && \
npm run worker:restart
claude-mem restart
```
## Fix: Stale PID File
@@ -70,7 +70,7 @@ curl -s http://127.0.0.1:37777/health
mkdir -p ~/.claude-mem && \
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json && \
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:restart && \
claude-mem restart && \
sleep 2 && \
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 && \
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;" && \
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:restart
claude-mem restart
```
**If integrity check fails, recreate database:**
@@ -94,7 +94,7 @@ npm run worker:restart
# WARNING: This deletes all memory data
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.old && \
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:restart
claude-mem restart
```
## Fix: Clean Reinstall
@@ -135,7 +135,7 @@ find ~/.claude-mem/logs/ -name "worker-*.log" -mtime +7 -delete
# Restart worker for fresh log
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
```
**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:
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
```
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):
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
```
## Issue: Port Conflicts
@@ -194,7 +194,7 @@ Quick fixes for frequently encountered claude-mem problems.
mkdir -p ~/.claude-mem
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
```
## Issue: Database Corrupted
@@ -219,7 +219,7 @@ Quick fixes for frequently encountered claude-mem problems.
```bash
rm ~/.claude-mem/claude-mem.db
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
# Worker will create new database
```
@@ -173,7 +173,7 @@ If FTS5 counts don't match, triggers may have failed. Restart worker to rebuild:
```bash
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.
@@ -263,7 +263,7 @@ sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
```bash
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.archive
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
```
## Database Recovery
@@ -13,7 +13,7 @@ npm run worker:status
npm run worker:start
# Restart worker
npm run worker:restart
claude-mem restart
# Stop worker
npm run worker:stop
@@ -152,7 +152,7 @@ npm run worker:start
```bash
# Restart worker (stops and starts)
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
claude-mem restart
# Or manually stop and start
npm run worker:stop
@@ -219,7 +219,7 @@ npm run worker:start
**Port conflict:**
```bash
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
npm run worker:restart
claude-mem restart
```
**Stale PID file:**
@@ -261,14 +261,14 @@ If fails, backup and recreate database.
**Out of memory:**
Check if database is too large or memory leak. Restart:
```bash
npm run worker:restart
claude-mem restart
```
**Port conflict race condition:**
Another process grabbing port intermittently. Change port:
```bash
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
npm run worker:restart
claude-mem restart
```
## Worker Management Commands
@@ -284,7 +284,7 @@ npm run worker:start
npm run worker:stop
# Restart worker
npm run worker:restart
claude-mem restart
# View 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 |
| 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 |
| 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 |
+1 -1
View File
@@ -166,7 +166,7 @@ async function buildHooks() {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
js: '#!/usr/bin/env node'
}
});
+187 -73
View File
@@ -6,14 +6,18 @@
* 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 { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { logger } from '../utils/logger.js';
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
@@ -35,6 +39,72 @@ const TOOL_ENDPOINT_MAP: Record<string, string> = {
'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
*/
@@ -182,25 +252,47 @@ async function verifyWorkerConnection(): Promise<boolean> {
/**
* 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 = [
{
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',
description: 'Search memory',
inputSchema: z.object({
query: z.string().optional(),
type: z.enum(['observations', 'sessions', 'prompts']).optional(),
obs_type: z.string().optional(),
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')
}),
description: 'Search memory. All parameters optional - call get_schema("search") for details',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: true
},
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['search'];
return await callWorkerAPI(endpoint, args);
@@ -208,17 +300,12 @@ const tools = [
},
{
name: 'timeline',
description: 'Timeline context',
inputSchema: z.object({
query: z.string().optional(),
anchor: z.number().optional(),
depth_before: z.number().min(0).max(100).default(10),
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()
}),
description: 'Timeline context. All parameters optional - call get_schema("timeline") for details',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: true
},
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['timeline'];
return await callWorkerAPI(endpoint, args);
@@ -226,16 +313,12 @@ const tools = [
},
{
name: 'get_recent_context',
description: 'Recent context',
inputSchema: z.object({
limit: z.number().min(1).max(100).default(30),
type: z.string().optional(),
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()
}),
description: 'Recent context. All parameters optional - call get_schema("get_recent_context") for details',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: true
},
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['get_recent_context'];
return await callWorkerAPI(endpoint, args);
@@ -243,16 +326,18 @@ const tools = [
},
{
name: 'get_context_timeline',
description: 'Timeline around ID',
inputSchema: z.object({
anchor: z.number(),
depth_before: z.number().min(0).max(100).default(10),
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()
}),
description: 'Timeline around observation ID',
inputSchema: {
type: 'object',
properties: {
anchor: {
type: 'number',
description: 'Observation ID (required). Optional params: get_schema("get_context_timeline")'
}
},
required: ['anchor'],
additionalProperties: true
},
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['get_context_timeline'];
return await callWorkerAPI(endpoint, args);
@@ -260,10 +345,12 @@ const tools = [
},
{
name: 'help',
description: 'Usage help',
inputSchema: z.object({
topic: z.enum(['workflow', 'search_params', 'examples', 'all']).default('all')
}),
description: 'Get detailed docs. All parameters optional - call get_schema("help") for details',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: true
},
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['help'];
return await callWorkerAPI(endpoint, args);
@@ -271,43 +358,70 @@ const tools = [
},
{
name: 'get_observation',
description: 'Fetch by ID',
inputSchema: z.object({
id: z.number()
}),
description: 'Fetch observation by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'Observation ID (required)'
}
},
required: ['id']
},
handler: async (args: any) => {
return await callWorkerAPIWithPath('/api/observation', args.id);
}
},
{
name: 'get_observations',
description: 'Batch fetch',
inputSchema: z.object({
ids: z.array(z.number()),
orderBy: z.enum(['date_desc', 'date_asc']).optional(),
limit: z.number().optional(),
project: z.string().optional()
}),
description: 'Batch fetch observations',
inputSchema: {
type: 'object',
properties: {
ids: {
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) => {
return await callWorkerAPIPost('/api/observations/batch', args);
}
},
{
name: 'get_session',
description: 'Session by ID',
inputSchema: z.object({
id: z.number()
}),
description: 'Fetch session by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'Session ID (required)'
}
},
required: ['id']
},
handler: async (args: any) => {
return await callWorkerAPIWithPath('/api/session', args.id);
}
},
{
name: 'get_prompt',
description: 'Prompt by ID',
inputSchema: z.object({
id: z.number()
}),
description: 'Fetch prompt by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'Prompt ID (required)'
}
},
required: ['id']
},
handler: async (args: any) => {
return await callWorkerAPIWithPath('/api/prompt', args.id);
}
@@ -333,7 +447,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: zodToJsonSchema(tool.inputSchema) as Record<string, unknown>
inputSchema: tool.inputSchema
}))
};
});
@@ -382,7 +496,7 @@ async function main() {
if (!workerAvailable) {
logger.warn('SYSTEM', 'Worker not available', undefined, { workerUrl: WORKER_BASE_URL });
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 {
logger.info('SYSTEM', 'Worker available', undefined, { workerUrl: WORKER_BASE_URL });
}
+115 -21
View File
@@ -5,6 +5,7 @@ import { spawn, spawnSync } from 'child_process';
import { homedir } from 'os';
import { DATA_DIR } from '../../shared/paths.js';
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
const PID_FILE = join(DATA_DIR, 'worker.pid');
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_FETCH_TIMEOUT_MS = 1000;
const PROCESS_EXIT_CHECK_INTERVAL_MS = 100;
const HTTP_SHUTDOWN_TIMEOUT_MS = 2000;
interface PidInfo {
pid: number;
@@ -99,8 +101,9 @@ export class ProcessManager {
const escapedBunPath = this.escapePowerShellString(bunPath);
const escapedScript = this.escapePowerShellString(script);
const escapedWorkDir = this.escapePowerShellString(MARKETPLACE_ROOT);
const escapedLogFile = this.escapePowerShellString(logFile);
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], {
stdio: 'pipe',
@@ -171,34 +174,65 @@ export class ProcessManager {
static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise<boolean> {
const info = this.getPidInfo();
if (!info) return true;
try {
if (process.platform === 'win32') {
// On Windows, use taskkill /T /F to kill entire process tree
if (process.platform === 'win32') {
// Windows: Try graceful HTTP shutdown first - this works regardless of PID file state
// 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
// which is necessary to properly release the socket and avoid zombie ports
const { execSync } = await import('child_process');
try {
execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
} catch {
// Process may already be dead
}
} else {
// On Unix, use signals
execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
} catch {
// Process may already be dead
}
// Wait for process to actually exit before removing PID file
try {
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');
await this.waitForExit(info.pid, timeout);
}
} catch {
try {
process.kill(info.pid, 'SIGKILL');
} catch {
// Process already dead
try {
process.kill(info.pid, 'SIGKILL');
} catch {
// Process already dead
}
}
}
this.removePidFile();
return true;
this.removePidFile();
return true;
}
}
static async restart(port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
@@ -229,6 +263,66 @@ export class ProcessManager {
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
private static getPidInfo(): PidInfo | null {
try {
+20 -11
View File
@@ -32,7 +32,7 @@ import { TimelineService } from './worker/TimelineService.js';
import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js';
// 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 { SessionRoutes } from './worker/http/routes/SessionRoutes.js';
import { DataRoutes } from './worker/http/routes/DataRoutes.js';
@@ -178,26 +178,35 @@ export class WorkerService {
// Instructions endpoint - loads SKILL.md sections on-demand for progressive instruction loading
this.app.get('/api/instructions', async (req, res) => {
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/)
// 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 {
const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
let content: string;
// Extract section based on topic
const section = this.extractInstructionSection(fullContent, topic);
if (operation) {
// 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
res.json({
content: [{
type: 'text',
text: section
text: content
}]
});
} 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({
content: [{
type: 'text',
@@ -208,8 +217,8 @@ export class WorkerService {
}
});
// Admin endpoints for process management
this.app.post('/api/admin/restart', async (_req, res) => {
// Admin endpoints for process management (localhost-only)
this.app.post('/api/admin/restart', requireLocalhost, async (_req, res) => {
res.json({ status: 'restarting' });
// On Windows, if managed by wrapper, send message to parent to handle restart
@@ -230,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' });
// On Windows, if managed by wrapper, send message to parent to handle shutdown
+11 -6
View File
@@ -3,11 +3,15 @@
*
* This wrapper exists to solve the Windows zombie port problem.
* The wrapper spawns the actual worker as a child process.
* When restart/shutdown is requested, the wrapper kills the child
* and respawns it (or exits), ensuring clean socket cleanup.
* When shutdown is requested, the wrapper kills the child and exits.
* The hooks will start a fresh wrapper+worker if needed.
*
* The wrapper itself has no sockets, so Bun's socket cleanup bug
* 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';
@@ -51,10 +55,11 @@ function spawnInner() {
log(`Inner exited with code=${code}, signal=${signal}`);
inner = null;
// If inner crashed unexpectedly (not during shutdown), respawn it
if (!isShuttingDown && code !== 0) {
log('Inner crashed, respawning in 1 second...');
setTimeout(() => spawnInner(), 1000);
// Don't auto-restart - let hooks handle it via ensureWorkerRunning()
// Auto-restart causes PID file mismatches and potential infinite loops
if (!isShuttingDown) {
log('Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)');
process.exit(code ?? 1);
}
});
+28
View File
@@ -60,6 +60,34 @@ export function createMiddleware(
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
* Used to avoid logging sensitive data or large payloads
@@ -7,7 +7,7 @@
import express, { Request, Response } from 'express';
import path from 'path';
import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { getPackageRoot } from '../../../../shared/paths.js';
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
import { DatabaseManager } from '../../DatabaseManager.js';
@@ -41,7 +41,19 @@ export class ViewerRoutes extends BaseRouteHandler {
*/
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
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');
res.setHeader('Content-Type', 'text/html');
res.send(html);
+2 -16
View File
@@ -24,18 +24,6 @@ export function getWorkerRestartInstructions(
actualError
} = 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
const prefix = customPrefix || 'Worker service connection failed.';
const portInfo = port ? ` (port ${port})` : '';
@@ -43,10 +31,8 @@ export function getWorkerRestartInstructions(
let message = `${prefix}${portInfo}\n\n`;
message += `To restart the worker:\n`;
message += `1. Exit Claude Code completely\n`;
message += `2. Open ${terminal}\n`;
message += `3. Navigate to: ${pluginDir}\n`;
message += `4. Run: npm run worker:restart\n`;
message += `5. Restart Claude Code`;
message += `2. Run: claude-mem restart\n`;
message += `3. Restart Claude Code`;
if (includeSkillFallback) {
message += `\n\nIf that doesn't work, try: /troubleshoot`;
@@ -48,7 +48,7 @@ describe('Hook Error Logging', () => {
handleFetchError(mockResponse, errorText, context);
} catch (error: any) {
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(() => {
handleWorkerError(connError);
}).toThrow('npm run worker:restart');
}).toThrow('claude-mem restart');
});
it('re-throws non-connection errors unchanged', () => {
@@ -130,7 +130,7 @@ describe('Hook Error Logging', () => {
expect.fail('Should have thrown');
} catch (error: any) {
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);
} catch (error: any) {
// 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)
expect(error.message).not.toContain('ECONNREFUSED');