Compare commits

...

38 Commits

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 17:39:58 -05:00
Alex Newman 1391df4fe8 chore: update CHANGELOG.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 17:27:35 -05:00
Alex Newman 71b29af00a chore: update version to 7.4.2 and replace mem-search.zip 2025-12-20 17:26:56 -05:00
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
Alex Newman 130abe04a9 chore: bump version to 7.3.7 2025-12-17 18:47:04 -05:00
Alex Newman bff10d49c9 fix(windows): Windows platform stabilization improvements (#378)
* chore: bump version to 7.3.6 in package.json

* Enhance worker readiness checks and MCP connection handling

- Updated health check endpoint to /api/readiness for better initialization tracking.
- Increased timeout for health checks and worker startup retries, especially for Windows.
- Added initialization flags to track MCP readiness and overall worker initialization status.
- Implemented a timeout guard for MCP connection to prevent hanging.
- Adjusted logging to reflect readiness state and errors more accurately.

* fix(windows): use Bun PATH detection in worker wrapper

Phase 2/8: Fix Bun PATH Detection in Worker Wrapper

- Import getBunPath() in worker-wrapper.ts for Bun detection
- Add Bun path resolution before spawning inner worker process
- Update spawn call to use detected Bun path instead of process.execPath
- Add logging to bun-path.ts when PATH detection succeeds
- Add logging when fallback paths are used
- Add Windows-specific validation for .exe extension
- Log warning with searched paths when Bun not found
- Fail fast with clear error message if Bun cannot be detected

This ensures worker-wrapper uses the correct Bun executable on Windows
even when Bun is not in PATH, fixing issue #371 where users reported
"Bun not in PATH" errors despite Bun being installed.

Addresses: #371

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

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

* fix(windows): standardize child process spawning with windowsHide

Phase 3/8: Standardize Child Process Spawning (Windows)

Changes:
- Added windowsHide flag to ChromaSync MCP subprocess spawn
- Added Windows-specific process tracking (childPid) in ChromaSync
- Force-kill subprocess on Windows before closing transport to prevent zombie processes
- Updated cleanupOrphanedProcesses() to support Windows using PowerShell Get-CimInstance
- Use taskkill /T /F for proper process tree cleanup on Windows
- Audited BranchManager - confirmed windowsHide already present on all spawn calls

This prevents PowerShell windows from appearing during ChromaSync operations
and ensures proper cleanup of subprocess trees on Windows.

Addresses: #363, #361, #367, #371, #373, #374

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

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

* fix(windows): enhance socket cleanup with recursive process tree management

Phase 4/8: Enhanced Socket Cleanup & Process Tree Management

Changes:
- Added recursive process tree enumeration in worker-wrapper.ts for Windows
- Enhanced killInner() to enumerate all descendants before killing
- Added fallback individual process kill if taskkill /T fails
- Added 10s timeout to ChromaSync.close() in DatabaseManager to prevent hangs
- Force nullify ChromaSync even on close failure to prevent resource leaks
- Improved logging to show full process tree during cleanup

This ensures complete cleanup of all child processes (ChromaSync MCP subprocess,
Python processes, etc.) preventing socket leaks and CLOSE_WAIT states.

Addresses: #363, #361

* fix(windows): consolidate project name extraction with drive root handling

Phase 5/8: Project Name Extraction Consolidation

- Created shared getProjectName() utility in src/utils/project-name.ts
- Handles edge case: drive roots (C:\, J:\) now return "drive-X" format
- Handles edge case: null/undefined/empty cwd now returns "unknown-project"
- Fixed missing null check bug in new-hook.ts
- Replaced duplicated path.basename(cwd) logic in:
  - src/hooks/context-hook.ts
  - src/hooks/new-hook.ts
  - src/services/context-generator.ts

Addresses: #374

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

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

* fix(windows): increase timeouts and improve error messages

Phase 6/8: Increase Timeouts & Improve Error Messages

- Enhanced logger.ts with platform prefix (WIN32/DARWIN) and PID in all logs
- Added comprehensive Windows troubleshooting to ProcessManager error messages
- Enhanced Bun detection error message with Windows-specific troubleshooting
- All error messages now include GitHub issue numbers and docs links
- Windows timeout already increased to 2.0x multiplier in previous phases

Changes:
- src/utils/logger.ts: Added platform prefix and PID to all log output
- src/services/process/ProcessManager.ts: Enhanced error messages with troubleshooting steps
- src/utils/bun-path.ts: Added Windows-specific Bun detection error guidance

Addresses: #363, #361, #367, #371, #373, #374

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

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

* fix(windows): add comprehensive Windows CI testing

Phase 7/8: Add Windows CI Testing

- Create automated Windows testing workflow
- Test worker startup/shutdown cycles
- Verify Bun PATH detection on Windows
- Test rapid restart scenarios
- Validate port cleanup after shutdown
- Check for zombie processes
- Run on all pushes and PRs to main/fix/feature branches

Addresses: #363, #361, #367, #371, #373, #374

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

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

* ci(windows): remove build steps from Windows CI workflow

Build files are already included in the plugin folder, so npm install
and npm run build are unnecessary steps in the CI workflow.

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

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

* revert: remove Windows CI workflow

The CI workflow cannot be properly implemented in the current architecture
due to limitations in testing the worker service in CI environments.

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

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

* security: add PID validation and improve ChromaSync timeout handling

Address critical security and reliability issues identified in PR review:

**Security Fixes:**
- Add PID validation before all PowerShell/taskkill command execution
- Validate PIDs are positive integers to prevent command injection
- Apply validation in worker-wrapper.ts, worker-service.ts, and ChromaSync.ts

**Reliability Improvements:**
- Add timeout handling to ChromaSync client.close() (10s timeout)
- Add timeout handling to ChromaSync transport.close() (5s timeout)
- Implement force-kill fallback when ChromaSync close operations timeout
- Prevents hanging on shutdown and ensures subprocess cleanup

**Implementation Details:**
- PID validation checks: Number.isInteger(pid) && pid > 0
- Applied before all execSync taskkill calls on Windows
- Applied in process enumeration (Get-CimInstance) PowerShell commands
- ChromaSync.close() uses Promise.race for timeout enforcement
- Graceful degradation with force-kill fallback on timeout

Addresses PR #378 review feedback

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

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

* Refactor ChromaSync client and transport closure logic

- Removed timeout handling for closing the Chroma client and transport.
- Simplified error logging for client and transport closure.
- Ensured subprocess cleanup logic is more straightforward.

* fix(worker): streamline Windows process management and cleanup

* revert: remove speculative LLM-generated complexity

Reverts defensive code that was added speculatively without user-reported issues:

- ChromaSync: Remove PID extraction and explicit taskkill (wrapper handles this)
- worker-wrapper: Restore simple taskkill /T /F (validated in v7.3.5)
- DatabaseManager: Remove Promise.race timeout wrapper
- hook-constants: Restore original timeout values
- logger: Remove platform/PID additions to every log line
- bun-path: Remove speculative logging

Keeps only changes that map to actual GitHub issues:
- #374: Drive root project name fix (getProjectName utility)
- #363: Readiness endpoint and Windows orphan cleanup
- #367: windowsHide on ChromaSync transport

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

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 18:44:04 -05:00
Alex Newman 40a71d3250 chore: update CHANGELOG.md 2025-12-17 16:02:04 -05:00
Alex Newman ae3d20c71a chore: bump version to 7.3.6 2025-12-17 16:01:07 -05:00
Alex Newman 54ef9662c1 Enhance SDKAgent response handling and message processing
- Updated response logging to process both empty and non-empty responses.
- Added functionality to mark messages as processed even when the response is empty.
- Refactored message processing logic to ensure all pending messages are marked as processed after successful observation/summary storage.
- Introduced a new private method `markMessagesProcessed` to encapsulate the logic for marking messages as processed, preventing message loss and duplicate processing.
2025-12-17 16:00:12 -05:00
Alex Newman 9aec461e14 Update mem-search plugin with new features and improvements 2025-12-17 15:39:03 -05:00
Alex Newman 0fe0705133 chore: bump version to 7.3.5 (#375)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 14:54:13 -05:00
ToxMox a5bf653a47 fix(windows): solve zombie port problem with wrapper architecture (#372)
On Windows, Bun doesn't properly release socket handles when the worker
process exits, causing "zombie ports" that remain bound even after all
processes are dead. This required a system reboot to clear.

Solution: Introduce a wrapper process (worker-wrapper.cjs) that:
- Spawns the actual worker as a child with IPC channel
- On restart/shutdown, uses `taskkill /T /F` to kill the entire process tree
- Exits itself, allowing hooks to start fresh

The wrapper has no sockets, so Bun's socket cleanup bug doesn't affect it.
When the wrapper kills the inner worker tree and exits, the port is properly
released and can be immediately reused.

Key changes:
- New worker-wrapper.ts for Windows process lifecycle management
- ProcessManager starts wrapper on Windows, worker directly on Unix
- Worker sends IPC messages to wrapper for restart/shutdown
- Health endpoint now includes debug info (build ID, managed status, hasIpc)

Tested: Restart API now properly releases port and new worker binds to same port.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 14:45:41 -05:00
Alex Newman 1fec1e8339 chore: update mem-search.zip with latest changes 2025-12-16 22:17:58 -05:00
Alex Newman 1afb14d0d6 docs: update CHANGELOG.md for v7.3.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 22:09:13 -05:00
Alex Newman e961cd5a4a chore: bump version to 7.3.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 22:08:20 -05:00
Alex Newman 660c523ba4 fix: shorten MCP server name to prevent tool name length errors (#360)
* fix: shorten MCP server name to prevent tool name length errors (#358)

Root cause: Claude Code prefixes MCP tool names with
`mcp__plugin_{plugin-name}_{server-name}__` which was 43 chars
for `mcp__plugin_claude-mem_claude-mem-search__`. Combined with
`progressive_description` (22 chars) this exceeded the 64 char limit.

Changes:
- Shortened MCP server name from 'claude-mem-search' to 'mem-search'
  (saves 8 chars, new prefix is 35 chars)
- Renamed `progressive_description` tool to `help` (saves 18 chars)
- Updated SKILL.md to reference new `help` tool name
- Updated internal Server constructor name for consistency

All tool names now safely under 64 char limit:
- Longest is now `get_batch_observations` at 56 chars total
- `help` is only 39 chars total

Fixes #358

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

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

* refactor: rename get_batch_observations to get_observations

The plural form naturally implies multiple items can be fetched,
following WordPress conventions. Simpler and clearer naming.

Also saves 6 additional characters for MCP tool name length.

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

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

* docs: update all references to renamed MCP tools

Updated documentation and code comments to reflect:
- progressive_description → help
- get_batch_observations → get_observations

Files updated:
- docs/public/usage/claude-desktop.mdx
- docs/public/architecture/worker-service.mdx
- src/services/worker/FormattingService.ts

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

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 22:06:24 -05:00
Alex Newman da30aedb28 docs: add Pro Features Architecture section to CLAUDE.md 2025-12-16 21:03:42 -05:00
Alex Newman 10e58ef221 chore: update plugin version to 7.3.3 and modify mem-search.zip 2025-12-16 18:02:10 -05:00
Alex Newman 5e97d539a5 docs: update CHANGELOG.md for v7.3.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:01:56 -05:00
63 changed files with 1986 additions and 2713 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [ "plugins": [
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "7.3.3", "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"
} }
+3
View File
@@ -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
View File
@@ -1,3 +1,4 @@
datasets/
node_modules/ node_modules/
dist/ dist/
*.log *.log
+182 -2141
View File
File diff suppressed because it is too large Load Diff
+22 -7
View File
@@ -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
@@ -77,6 +72,26 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created
**Source**: `docs/public/` - MDX files, edit `docs.json` for navigation **Source**: `docs/public/` - MDX files, edit `docs.json` for navigation
**Deploy**: Auto-deploys from GitHub on push to main **Deploy**: Auto-deploys from GitHub on push to main
## Pro Features Architecture
Claude-mem is designed with a clean separation between open-source core functionality and optional Pro features.
**Open-Source Core** (this repository):
- All worker API endpoints on localhost:37777 remain fully open and accessible
- Pro features are headless - no proprietary UI elements in this codebase
- Pro integration points are minimal: settings for license keys, tunnel provisioning logic
- The architecture ensures Pro features extend rather than replace core functionality
**Pro Features** (coming soon, external):
- Enhanced UI (Memory Stream) connects to the same localhost:37777 endpoints as the open viewer
- Additional features like advanced filtering, timeline scrubbing, and search tools
- Access gated by license validation, not by modifying core endpoints
- Users without Pro licenses continue using the full open-source viewer UI without limitation
This architecture preserves the open-source nature of the project while enabling sustainable development through optional paid features.
# Important # Important
No need to edit the changelog ever, it's generated automatically. No need to edit the changelog ever, it's generated automatically.
+1 -1
View File
@@ -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
``` ```
+2 -2
View File
@@ -240,7 +240,7 @@ POST /api/observations/batch
- `400 Bad Request`: `{"error": "ids must be an array of numbers"}` - `400 Bad Request`: `{"error": "ids must be an array of numbers"}`
- `400 Bad Request`: `{"error": "All ids must be integers"}` - `400 Bad Request`: `{"error": "All ids must be integers"}`
**Use Case**: This endpoint is used by the `get_batch_observations` MCP tool to efficiently retrieve multiple observations in a single request, avoiding the overhead of multiple individual requests. **Use Case**: This endpoint is used by the `get_observations` MCP tool to efficiently retrieve multiple observations in a single request, avoiding the overhead of multiple individual requests.
#### 9. Get Session by ID #### 9. Get Session by ID
``` ```
@@ -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
+5 -5
View File
@@ -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:
+2 -2
View File
@@ -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
``` ```
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
``` ```
+8 -8
View File
@@ -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"
+2 -2
View File
@@ -118,12 +118,12 @@ The skill provides access to these MCP tools:
| `search` | Unified search across observations, sessions, and prompts | | `search` | Unified search across observations, sessions, and prompts |
| `timeline` | Get chronological context around a query or observation ID | | `timeline` | Get chronological context around a query or observation ID |
| `get_observation` | Fetch a single observation by ID | | `get_observation` | Fetch a single observation by ID |
| `get_batch_observations` | Fetch multiple observations efficiently | | `get_observations` | Fetch multiple observations efficiently |
| `get_session` | Fetch session summary by ID | | `get_session` | Fetch session summary by ID |
| `get_prompt` | Fetch user prompt by ID | | `get_prompt` | Fetch user prompt by ID |
| `get_recent_context` | Get recent timeline items | | `get_recent_context` | Get recent timeline items |
| `get_context_timeline` | Get timeline around a specific observation | | `get_context_timeline` | Get timeline around a specific observation |
| `progressive_description` | Load detailed usage instructions | | `help` | Load detailed usage instructions |
## Troubleshooting ## Troubleshooting
+1 -1
View File
@@ -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
+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>` 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
+1 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "7.3.3", "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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "7.3.3", "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
View File
@@ -1,6 +1,6 @@
{ {
"mcpServers": { "mcpServers": {
"claude-mem-search": { "mem-search": {
"type": "stdio", "type": "stdio",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.cjs" "command": "${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.cjs"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem-plugin", "name": "claude-mem-plugin",
"version": "7.3.2", "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
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 * 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
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bun
"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.
+124 -9
View File
@@ -86,13 +86,13 @@ For each relevant ID, fetch full details using MCP tools:
**Fetch multiple observations (ALWAYS use for 2+ IDs):** **Fetch multiple observations (ALWAYS use for 2+ IDs):**
``` ```
get_batch_observations(ids=[11131, 10942, 10855]) get_observations(ids=[11131, 10942, 10855])
``` ```
**With ordering and limit:** **With ordering and limit:**
``` ```
get_batch_observations( get_observations(
ids=[11131, 10942, 10855], ids=[11131, 10942, 10855],
orderBy="date_desc", orderBy="date_desc",
limit=10, limit=10,
@@ -126,7 +126,7 @@ get_prompt(id=5421)
**Batch optimization:** **Batch optimization:**
- **ALWAYS use `get_batch_observations` for 2+ observations** - **ALWAYS use `get_observations` for 2+ observations**
- 10-100x more efficient than individual fetches - 10-100x more efficient than individual fetches
- Single HTTP request vs N requests - Single HTTP request vs N requests
- Returns all results in one response - Returns all results in one response
@@ -175,13 +175,13 @@ search(query="database migration", limit=20, project="my-project")
**Get detailed instructions:** **Get detailed instructions:**
Use the `progressive_description` tool to load full instructions on-demand: Use the `help` tool to load full instructions on-demand:
``` ```
progressive_description(topic="workflow") # Get 4-step workflow help(topic="workflow") # Get 4-step workflow
progressive_description(topic="search_params") # Get parameters reference help(topic="search_params") # Get parameters reference
progressive_description(topic="examples") # Get usage examples help(topic="examples") # Get usage examples
progressive_description(topic="all") # Get complete guide help(topic="all") # Get complete guide
``` ```
## Why This Workflow? ## Why This Workflow?
@@ -210,5 +210,120 @@ progressive_description(topic="all") # Get complete guide
**Remember:** **Remember:**
- ALWAYS get timeline context to understand what was happening - ALWAYS get timeline context to understand what was happening
- ALWAYS use `get_batch_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 |
+31 -1
View File
@@ -26,6 +26,11 @@ const WORKER_SERVICE = {
source: 'src/services/worker-service.ts' source: 'src/services/worker-service.ts'
}; };
const WORKER_WRAPPER = {
name: 'worker-wrapper',
source: 'src/services/worker-wrapper.ts'
};
const MCP_SERVER = { const MCP_SERVER = {
name: 'mcp-server', name: 'mcp-server',
source: 'src/servers/mcp-server.ts' source: 'src/servers/mcp-server.ts'
@@ -120,6 +125,31 @@ async function buildHooks() {
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`); const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`); console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
// Build worker wrapper (Windows zombie port fix)
console.log(`\n🔧 Building worker wrapper...`);
await build({
entryPoints: [WORKER_WRAPPER.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${WORKER_WRAPPER.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make worker wrapper executable
fs.chmodSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`, 0o755);
const wrapperStats = fs.statSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`);
console.log(`✓ worker-wrapper built (${(wrapperStats.size / 1024).toFixed(2)} KB)`);
// Build MCP server // Build MCP server
console.log(`\n🔧 Building MCP server...`); console.log(`\n🔧 Building MCP server...`);
await build({ await build({
@@ -136,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'
} }
}); });
+137
View File
@@ -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();
+2 -2
View File
@@ -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)}`;
+2 -2
View File
@@ -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();
+192 -78
View File
@@ -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';
@@ -32,7 +36,73 @@ const TOOL_ENDPOINT_MAP: Record<string, string> = {
'timeline': '/api/timeline', 'timeline': '/api/timeline',
'get_recent_context': '/api/context/recent', 'get_recent_context': '/api/context/recent',
'get_context_timeline': '/api/context/timeline', 'get_context_timeline': '/api/context/timeline',
'progressive_description': '/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 }
}
}; };
/** /**
@@ -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,71 +326,102 @@ 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);
} }
}, },
{ {
name: 'progressive_description', 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['progressive_description']; const endpoint = TOOL_ENDPOINT_MAP['help'];
return await callWorkerAPI(endpoint, args); return await callWorkerAPI(endpoint, args);
} }
}, },
{ {
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_batch_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);
} }
@@ -317,7 +431,7 @@ const tools = [
// Create the MCP server // Create the MCP server
const server = new Server( const server = new Server(
{ {
name: 'claude-mem-search-server', name: 'mem-search-server',
version: '1.0.0', version: '1.0.0',
}, },
{ {
@@ -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 });
} }
+2 -1
View File
@@ -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 {
+144 -21
View File
@@ -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;
@@ -43,8 +45,10 @@ export class ProcessManager {
// Ensure log directory exists // Ensure log directory exists
mkdirSync(LOG_DIR, { recursive: true }); mkdirSync(LOG_DIR, { recursive: true });
// Get worker script path // On Windows, use the wrapper script to solve zombie port problem
const workerScript = join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'); // On Unix, use the worker directly
const scriptName = process.platform === 'win32' ? 'worker-wrapper.cjs' : 'worker-service.cjs';
const workerScript = join(MARKETPLACE_ROOT, 'plugin', 'scripts', scriptName);
if (!existsSync(workerScript)) { if (!existsSync(workerScript)) {
return { success: false, error: `Worker script not found at ${workerScript}` }; return { success: false, error: `Worker script not found at ${workerScript}` };
@@ -86,6 +90,10 @@ export class ProcessManager {
// Note: windowsHide: true doesn't work with detached: true (Bun inherits Node.js process spawning semantics) // Note: windowsHide: true doesn't work with detached: true (Bun inherits Node.js process spawning semantics)
// See: https://github.com/nodejs/node/issues/21825 and PR #315 for detailed testing // See: https://github.com/nodejs/node/issues/21825 and PR #315 for detailed testing
// //
// On Windows, we start worker-wrapper.cjs which manages the actual worker-service.cjs.
// This solves the zombie port problem: the wrapper has no sockets, so when it kills
// and respawns the inner worker, the socket is properly released.
//
// Security: All paths (bunPath, script, MARKETPLACE_ROOT) are application-controlled system paths, // Security: All paths (bunPath, script, MARKETPLACE_ROOT) are application-controlled system paths,
// not user input. If an attacker could modify these paths, they would already have full filesystem // not user input. If an attacker could modify these paths, they would already have full filesystem
// access including direct access to ~/.claude-mem/claude-mem.db. Nevertheless, we properly escape // access including direct access to ~/.claude-mem/claude-mem.db. Nevertheless, we properly escape
@@ -93,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',
@@ -165,21 +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') {
process.kill(info.pid, 'SIGTERM'); // Windows: Try graceful HTTP shutdown first - this works regardless of PID file state
await this.waitForExit(info.pid, timeout); // because the worker shuts itself down from the inside (via wrapper IPC)
} catch { const port = info?.port ?? this.getPortFromSettings();
try { const httpShutdownSucceeded = await this.tryHttpShutdown(port);
process.kill(info.pid, 'SIGKILL');
} catch { if (httpShutdownSucceeded) {
// Process already dead // HTTP shutdown succeeded - worker confirmed down, safe to remove PID file
this.removePidFile();
return true;
} }
}
this.removePidFile(); // HTTP shutdown failed (worker not responding), fall back to taskkill
return true; 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
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
}
}
this.removePidFile();
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 }> {
@@ -210,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 {
@@ -252,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> {
+13 -2
View File
@@ -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',
+268 -52
View File
@@ -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;
@@ -118,18 +122,46 @@ export class WorkerService {
*/ */
private setupRoutes(): void { private setupRoutes(): void {
// Health check endpoint // Health check endpoint
// TEST_BUILD_ID helps verify which build is running during debugging
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
this.app.get('/api/health', (_req, res) => { this.app.get('/api/health', (_req, res) => {
res.status(200).json({ status: 'ok' }); res.status(200).json({
status: 'ok',
build: TEST_BUILD_ID,
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
hasIpc: typeof process.send === 'function',
platform: process.platform,
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) {
@@ -146,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',
@@ -176,21 +217,46 @@ 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' });
setTimeout(async () => {
await this.shutdown(); // On Windows, if managed by wrapper, send message to parent to handle restart
process.exit(0); // This solves the Windows zombie port problem where sockets aren't properly released
}, 100); const isWindowsManaged = process.platform === 'win32' &&
process.env.CLAUDE_MEM_MANAGED === 'true' &&
process.send;
if (isWindowsManaged) {
logger.info('SYSTEM', 'Sending restart request to wrapper');
process.send!({ type: 'restart' });
} else {
// Unix or standalone Windows - handle restart ourselves
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
}
}); });
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' });
setTimeout(async () => {
await this.shutdown(); // On Windows, if managed by wrapper, send message to parent to handle shutdown
process.exit(0); const isWindowsManaged = process.platform === 'win32' &&
}, 100); process.env.CLAUDE_MEM_MANAGED === 'true' &&
process.send;
if (isWindowsManaged) {
logger.info('SYSTEM', 'Sending shutdown request to wrapper');
process.send!({ type: 'shutdown' });
} else {
// Unix or standalone Windows - handle shutdown ourselves
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
}
}); });
this.viewerRoutes.setupRoutes(this.app); this.viewerRoutes.setupRoutes(this.app);
@@ -261,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) {
@@ -287,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) {
@@ -346,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',
@@ -354,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) {
@@ -399,12 +512,32 @@ export class WorkerService {
/** /**
* Shutdown the worker service * Shutdown the worker service
*
* IMPORTANT: On Windows, we must kill all child processes before exiting
* to prevent zombie ports. The socket handle can be inherited by children,
* and if not properly closed, the port stays bound after process death.
*/ */
async shutdown(): Promise<void> { async shutdown(): Promise<void> {
// Shutdown all active sessions logger.info('SYSTEM', 'Shutdown initiated');
// STEP 1: Enumerate all child processes BEFORE we start closing things
const childPids = await this.getChildProcesses(process.pid);
logger.info('SYSTEM', 'Found child processes', { count: childPids.length, pids: childPids });
// STEP 2: Close HTTP server first
if (this.server) {
this.server.closeAllConnections();
await new Promise<void>((resolve, reject) => {
this.server!.close(err => err ? reject(err) : resolve());
});
this.server = null;
logger.info('SYSTEM', 'HTTP server closed');
}
// STEP 3: Shutdown active sessions
await this.sessionManager.shutdownAll(); await this.sessionManager.shutdownAll();
// Close MCP client connection (terminates MCP server process) // STEP 4: Close MCP client connection (signals child to exit gracefully)
if (this.mcpClient) { if (this.mcpClient) {
try { try {
await this.mcpClient.close(); await this.mcpClient.close();
@@ -414,19 +547,102 @@ export class WorkerService {
} }
} }
// Close HTTP server // STEP 5: Close database connection (includes ChromaSync cleanup)
if (this.server) {
await new Promise<void>((resolve, reject) => {
this.server!.close(err => err ? reject(err) : resolve());
});
}
// Close database connection (includes ChromaSync cleanup)
await this.dbManager.close(); await this.dbManager.close();
// STEP 6: Force kill any remaining child processes (Windows zombie port fix)
if (childPids.length > 0) {
logger.info('SYSTEM', 'Force killing remaining children');
for (const pid of childPids) {
await this.forceKillProcess(pid);
}
// Wait for children to fully exit
await this.waitForProcessesExit(childPids, 5000);
}
logger.info('SYSTEM', 'Worker shutdown complete'); logger.info('SYSTEM', 'Worker shutdown complete');
} }
/**
* Get all child process PIDs (Windows-specific)
*/
private async getChildProcesses(parentPid: number): Promise<number[]> {
if (process.platform !== 'win32') {
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 {
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
const { stdout } = await execAsync(cmd, { timeout: 5000 });
return stdout
.trim()
.split('\n')
.map(s => parseInt(s.trim(), 10))
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0); // SECURITY: Validate each PID
} catch (error) {
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
return [];
}
}
/**
* Force kill a process by PID (Windows: uses taskkill /F /T)
*/
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 {
if (process.platform === 'win32') {
// /T kills entire process tree, /F forces termination
await execAsync(`taskkill /PID ${pid} /T /F`, { timeout: 5000 });
logger.info('SYSTEM', 'Killed process', { pid });
} else {
process.kill(pid, 'SIGKILL');
}
} catch (error) {
// Process may already be dead, which is fine
logger.debug('SYSTEM', 'Process already dead or kill failed', { pid });
}
}
/**
* Wait for processes to fully exit
*/
private async waitForProcessesExit(pids: number[], timeoutMs: number): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const stillAlive = pids.filter(pid => {
try {
process.kill(pid, 0); // Signal 0 checks if process exists
return true;
} catch {
return false;
}
});
if (stillAlive.length === 0) {
logger.info('SYSTEM', 'All child processes exited');
return;
}
logger.debug('SYSTEM', 'Waiting for processes to exit', { stillAlive });
await new Promise(r => setTimeout(r, 100));
}
logger.warn('SYSTEM', 'Timeout waiting for child processes to exit');
}
/** /**
* 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
+157
View File
@@ -0,0 +1,157 @@
/**
* Worker Wrapper - Manages worker process lifecycle
*
* This wrapper exists to solve the Windows zombie port problem.
* The wrapper spawns the actual worker as a child process.
* 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';
import path from 'path';
const isWindows = process.platform === 'win32';
const SCRIPT_DIR = __dirname;
const INNER_SCRIPT = path.join(SCRIPT_DIR, 'worker-service.cjs');
let inner: ChildProcess | null = null;
let isShuttingDown = false;
function log(msg: string) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [wrapper] ${msg}`);
}
function spawnInner() {
log(`Spawning inner worker: ${INNER_SCRIPT}`);
inner = spawn(process.execPath, [INNER_SCRIPT], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { ...process.env, CLAUDE_MEM_MANAGED: 'true' },
cwd: path.dirname(INNER_SCRIPT),
});
inner.on('message', async (msg: { type: string }) => {
if (msg.type === 'restart' || msg.type === 'shutdown') {
// Both restart and shutdown: kill inner and exit wrapper
// The hooks will start a fresh wrapper+inner if needed
log(`${msg.type} requested by inner`);
isShuttingDown = true;
await killInner();
log('Exiting wrapper');
process.exit(0);
}
});
inner.on('exit', (code, signal) => {
log(`Inner exited with code=${code}, signal=${signal}`);
inner = null;
// 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);
}
});
inner.on('error', (err) => {
log(`Inner error: ${err.message}`);
});
}
async function killInner(): Promise<void> {
if (!inner || !inner.pid) {
log('No inner process to kill');
return;
}
const pid = inner.pid;
log(`Killing inner process tree (pid=${pid})`);
if (isWindows) {
// On Windows, use taskkill /T /F to kill entire process tree
// This ensures all children (MCP server, ChromaSync, etc.) are killed
// which is necessary to properly release the socket
try {
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
log(`taskkill completed for pid=${pid}`);
} catch (error) {
// Process may already be dead
log(`taskkill failed (process may be dead): ${error}`);
}
} else {
// On Unix, SIGTERM then SIGKILL
inner.kill('SIGTERM');
// Wait for exit with timeout
const exitPromise = new Promise<void>(resolve => {
if (!inner) {
resolve();
return;
}
inner.on('exit', () => resolve());
});
const timeoutPromise = new Promise<void>(resolve =>
setTimeout(() => resolve(), 5000)
);
await Promise.race([exitPromise, timeoutPromise]);
// Force kill if still alive
if (inner && !inner.killed) {
log('Inner did not exit gracefully, force killing');
inner.kill('SIGKILL');
}
}
// Wait for the process to fully exit
await waitForProcessExit(pid, 5000);
inner = null;
log('Inner process terminated');
}
async function waitForProcessExit(pid: number, timeoutMs: number): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
process.kill(pid, 0); // Check if process exists
await new Promise(r => setTimeout(r, 100));
} catch {
// Process is dead
return;
}
}
log(`Timeout waiting for process ${pid} to exit`);
}
// Handle wrapper signals
process.on('SIGTERM', async () => {
log('Wrapper received SIGTERM');
isShuttingDown = true;
await killInner();
process.exit(0);
});
process.on('SIGINT', async () => {
log('Wrapper received SIGINT');
isShuttingDown = true;
await killInner();
process.exit(0);
});
// Start the inner worker
log('Wrapper starting');
spawnInner();
+1 -1
View File
@@ -18,7 +18,7 @@ export class FormattingService {
💡 Search Strategy: 💡 Search Strategy:
1. Search with index to see titles, dates, IDs 1. Search with index to see titles, dates, IDs
2. Use timeline to get context around interesting results 2. Use timeline to get context around interesting results
3. Batch fetch full details: get_batch_observations(ids=[...]) 3. Batch fetch full details: get_observations(ids=[...])
Tips: Tips:
Filter by type: obs_type="bugfix,feature" Filter by type: obs_type="bugfix,feature"
+1 -1
View File
@@ -108,7 +108,7 @@ Settings and configuration (use domain services directly):
- Keep all existing behavior identical - Keep all existing behavior identical
**MCP vs Direct DB Split** (inherited, not changed in Phase 1): **MCP vs Direct DB Split** (inherited, not changed in Phase 1):
- Search operations → MCP server (claude-mem-search) - Search operations → MCP server (mem-search)
- Session/data operations → Direct DB access via domain services - Session/data operations → Direct DB access via domain services
## Future Phase 2 ## Future Phase 2
+13 -3
View File
@@ -113,7 +113,7 @@ export class SDKAgent {
// Calculate discovery tokens (delta for this response only) // Calculate discovery tokens (delta for this response only)
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse; const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
// Only log non-empty responses (filter out noise) // Process response (empty or not) and mark messages as processed
if (responseSize > 0) { if (responseSize > 0) {
const truncatedResponse = responseSize > 100 const truncatedResponse = responseSize > 100
? textContent.substring(0, 100) + '...' ? textContent.substring(0, 100) + '...'
@@ -125,6 +125,9 @@ export class SDKAgent {
// Parse and process response with discovery token delta // Parse and process response with discovery token delta
await this.processSDKResponse(session, textContent, worker, discoveryTokens); await this.processSDKResponse(session, textContent, worker, discoveryTokens);
} else {
// Empty response - still need to mark pending messages as processed
await this.markMessagesProcessed(session, worker);
} }
} }
@@ -396,8 +399,15 @@ export class SDKAgent {
} }
} }
// CRITICAL: Mark ALL pending messages as successfully processed // Mark messages as processed after successful observation/summary storage
// This prevents message loss if worker crashes before SDK finishes await this.markMessagesProcessed(session, worker);
}
/**
* Mark all pending messages as successfully processed
* CRITICAL: Prevents message loss and duplicate processing
*/
private async markMessagesProcessed(session: ActiveSession, worker: any | undefined): Promise<void> {
const pendingMessageStore = this.sessionManager.getPendingMessageStore(); const pendingMessageStore = this.sessionManager.getPendingMessageStore();
if (session.pendingProcessingIds.size > 0) { if (session.pendingProcessingIds.size > 0) {
for (const messageId of session.pendingProcessingIds) { for (const messageId of session.pendingProcessingIds) {
+28
View File
@@ -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);
+4 -3
View File
@@ -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
}); });
+2 -16
View File
@@ -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`;
+37
View File
@@ -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');