Compare commits

...

125 Commits

Author SHA1 Message Date
Alex Newman d3fb58ca75 chore: bump version to 7.1.11 2025-12-13 21:00:04 -05:00
Copilot 6a63a8d69c refactor: simplify hook execution - use Node directly instead of Bun (#290)
Removes bun-wrapper indirection. Hooks are compiled JavaScript that work perfectly with Node. Worker still uses Bun where performance matters. Fixes #264
2025-12-13 20:58:38 -05:00
Alex Newman 1ac0db25e5 docs: regenerate CHANGELOG from GitHub releases 2025-12-13 20:23:01 -05:00
Alex Newman 3e1d5fcd73 chore: bump version to 7.1.10 2025-12-13 20:22:14 -05:00
Alex Newman f41579b4d0 feat: auto-cleanup orphaned chroma-mcp processes on worker startup
Enhancement to process leak fix from v7.1.9 - automatically detects and
kills orphaned chroma-mcp processes when the worker starts.

Changes:
- Added cleanupOrphanedProcesses() method to WorkerService
- Scans for existing chroma-mcp processes on startup
- Kills all found processes before creating new ones
- Logs cleanup activity (process count and PIDs)
- Non-fatal error handling (continues on cleanup failure)

Benefits:
- Automatically recovers from pre-7.1.9 process leaks
- Ensures clean slate on every worker restart
- No manual intervention needed to cleanup orphans
- Prevents accumulation even if v7.1.9 close() fails

Verified working in logs:
[INFO] [SYSTEM] Cleaning up orphaned chroma-mcp processes {count=2, pids=33753,33750}
[INFO] [SYSTEM] Orphaned processes cleaned up {count=2}

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:21:21 -05:00
Alex Newman 0f3151cc2d docs: regenerate CHANGELOG from GitHub releases 2025-12-13 20:15:38 -05:00
Alex Newman e9370a915c chore: bump version to 7.1.9 2025-12-13 20:14:32 -05:00
Alex Newman 6d4a4819de fix: prevent chroma-mcp process leaks on worker restart
Critical bugfix: ChromaSync now properly cleans up chroma-mcp subprocesses
when the worker is restarted, preventing memory exhaustion from orphaned
processes accumulating over time.

Changes:
- Store reference to StdioClientTransport subprocess
- Explicitly close transport in close() method to kill subprocess
- Add error handling to ensure cleanup even on failures
- Reset all state in finally block

Problem:
Each worker restart spawned a new chroma-mcp process but never killed the
old one. After multiple restarts, orphaned processes accumulated (16+ seen
in production), consuming 900MB+ RAM and eventually causing OOM kills that
silently failed backfills.

Impact:
- Eliminates process accumulation
- Prevents memory exhaustion from leaked subprocesses
- Fixes silent backfill failures caused by OOM kills
- Ensures graceful cleanup on worker shutdown

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:13:05 -05:00
Alex Newman 2681a2d251 fix: handle backticks in issue bodies when converting to discussions
Consolidate issue fetching and discussion creation into single step
to avoid template literal injection issues. Issue data now stays in
JavaScript context instead of being passed through GitHub Actions
template interpolation, preventing syntax errors when issue bodies
contain backticks or other special characters.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 18:48:23 -05:00
Alex Newman c2eefe3578 feat: auto-convert feature requests to discussions
Add GitHub Action to automatically move feature request issues to
Discussions, keeping Issues tab focused on bug reports.

Changes:
- Update feature request template to add 'feature-request' label
- Create workflow to auto-convert labeled issues to discussions
- Support manual conversion via workflow_dispatch
- Auto-close and lock converted issues with discussion link

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 18:41:42 -05:00
Alex Newman dd5e2e57dd docs: regenerate CHANGELOG from GitHub releases 2025-12-13 17:55:08 -05:00
Alex Newman 266076da98 chore: bump version to 7.1.8 2025-12-13 17:54:11 -05:00
Alex Newman a0b4381dc8 Merge feature/import-export: Add memory export/import scripts with duplicate prevention 2025-12-13 17:53:18 -05:00
Alex Newman 4904d9c531 docs: regenerate CHANGELOG from GitHub releases 2025-12-13 17:49:43 -05:00
Alex Newman 4c44a65877 fix: remove Windows process.type workaround causing libuv crashes
Removed the process.type = 'renderer' workaround that was causing libuv
assertion failures on Windows. This hack was attempting to hide console
windows but resulted in crashes when process.title was accessed.

Prioritizing stability over cosmetics - console windows may briefly appear
on Windows until the MCP SDK provides proper window hiding support.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 17:48:47 -05:00
Alex Newman f6b310126c Update issue templates 2025-12-13 17:36:12 -05:00
Alex Newman 77220a76bf Fix formatting in FUNDING.yml for GitHub funding 2025-12-13 17:35:40 -05:00
Copilot 42ed414a4c Fix: Exclude developer-specific .mcp.json from marketplace releases (#277)
* Initial plan

* Fix: Remove developer-specific .mcp.json config and exclude from sync

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Fix: Use leading slash in rsync exclude to only exclude root .mcp.json

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Complete fix for developer-specific .mcp.json config issue

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-12-13 17:22:38 -05:00
Alex Newman 0185d765ce docs: regenerate CHANGELOG from GitHub releases
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 17:09:01 -05:00
Alex Newman 12c2ecce06 chore: bump version to 7.1.6
Improved error messages with platform-specific worker restart instructions

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 17:08:16 -05:00
Alex Newman bb0508d639 Refactor error handling to use platform-specific worker restart instructions
- Updated multiple hooks (context-hook, new-hook, save-hook, summary-hook, user-message-hook) to throw errors using `getWorkerRestartInstructions` for improved user guidance on worker connection issues.
- Enhanced `handleWorkerError` function to utilize the new error message generator for consistent error reporting.
- Modified `ensureWorkerRunning` function to provide detailed instructions based on the worker's state, including port information.
- Introduced `getWorkerRestartInstructions` utility in `error-messages.ts` to generate platform-aware error messages for worker failures.
2025-12-13 17:06:45 -05:00
Alex Newman f00ef33f86 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-12-13 16:54:24 -05:00
Alex Newman c270bd3177 chore: add FUNDING.yml to support GitHub Sponsors 2025-12-13 16:53:00 -05:00
Alex Newman 0836a97845 Add GitHub Actions workflow to summarize new issues (#278) 2025-12-13 16:19:18 -05:00
Alex Newman 19e285a209 docs: regenerate CHANGELOG from GitHub releases 2025-12-13 15:55:46 -05:00
Alex Newman ba877214c1 chore: bump version to 7.1.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 15:54:57 -05:00
Justin Kowarsch 3d4baefac2 fix: Use getWorkerHost() instead of hardcoded localhost in MCP server (#276)
On Windows systems, `localhost` resolves to IPv6 (::1) while the worker
binds to IPv4 (127.0.0.1), causing MCP tool connections to fail.

This change uses the existing getWorkerHost() function which correctly
returns the configured host address (defaulting to 127.0.0.1).

Fixes connection failures on Windows where localhost prefers IPv6.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 15:54:03 -05:00
Alex Newman 453b7857b8 docs: regenerate CHANGELOG from GitHub releases 2025-12-13 15:37:11 -05:00
Alex Newman c28417af00 chore: bump version to 7.1.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 15:36:21 -05:00
Jonas Hanisch 2f08db3c01 fix: add npm fallback when bun install fails with alias packages (#265)
* fix: add npm fallback when bun install fails with alias packages

Bun has issues resolving npm alias packages (e.g., string-width-cjs,
strip-ansi-cjs, wrap-ansi-cjs) that are defined in package-lock.json.
When bun fails with 404 errors for these packages, we now fall back
to npm which handles aliases correctly.

This fixes the installation failure that many users are experiencing
where bun install fails with:
  error: GET https://registry.npmjs.org/string-width-cjs/-/string-width-cjs-4.2.3.tgz - 404

The fallback is transparent to users - they will see a warning message
and the installation will continue with npm.

Fixes #262
Related: #261, #253

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

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

* fix: preserve original code style (single quotes)

---------

Co-authored-by: Jonas Hanisch <jhanisch@matero.de>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-13 15:34:13 -05:00
Alex Newman 672cb5d203 docs: regenerate CHANGELOG from GitHub releases
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 23:34:55 -05:00
Alex Newman cbe492dde3 chore: bump version to 7.1.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 23:33:53 -05:00
Alex Newman 0fb6f3cf4e refactor: streamline Bun and uv installation checks and paths 2025-12-12 23:29:42 -05:00
Alex Newman 5cd68f4a96 build: sync plugin build artifacts for v7.1.2
Updated built plugin files with latest changes including localhost
binding security improvements and enhanced runtime detection.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 23:03:27 -05:00
Alex Newman 10a8598aac docs: regenerate CHANGELOG from GitHub releases 2025-12-12 22:51:37 -05:00
Alex Newman 19af455c57 chore: bump version to 7.1.2
🐛 Bug Fixes

**Windows Installation**
- Fixed Bun PATH detection on Windows after fresh install
- Added fallback to check common install paths before PATH reload
- Improved smart-install.js to use full Bun path when not in PATH
- Added proper path quoting for Windows usernames with spaces

**Worker Startup**
- Fixed worker connection failures in Stop hook
- Added health check retry loop (5 attempts, 500ms intervals)
- Worker now waits up to 2.5s for responsiveness before returning
- Improved error detection for Bun's ConnectionRefused error format

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:50:53 -05:00
Alex Newman b5807aed2e docs: regenerate CHANGELOG from GitHub releases 2025-12-12 22:41:39 -05:00
Alex Newman c6fd984cc1 chore: bump version to 7.1.1
- Updated all version files
- Updated CHANGELOG with v7.1.1 release notes
- Critical fixes: Windows Bun auto-install, path quoting
- New feature: Automatic worker restart on version updates
2025-12-12 22:40:48 -05:00
Alex Newman 490ba182d5 feat: automatic worker restart on version updates
Critical improvement for seamless upgrades across all versions.

Changes:
1. Added /api/version endpoint to worker service
   - Returns current worker version from package.json

2. Added version checking in worker-utils.ts
   - getPluginVersion() - reads plugin's package.json version
   - getWorkerVersion() - fetches version from worker API
   - ensureWorkerVersionMatches() - compares and restarts if needed

3. Modified ensureWorkerRunning()
   - Now calls ensureWorkerVersionMatches() after health check
   - Automatically restarts worker when version mismatch detected
   - Logs version mismatch for debugging

Impact:
- Users no longer need to manually restart worker after upgrades
- Eliminates connection errors from running old worker code
- Critical for v7.1.1 (Bun auto-install) and all future releases
- Fixes the issue where PR #236 changes weren't applied until manual restart

Testing:
- Version endpoint working: returns {"version":"7.1.0"}
- Worker health check: passing
- Auto-restart logic: triggers on version mismatch

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:39:24 -05:00
Alex Newman 4baed97bd0 fix(windows): restore Bun auto-installation in smart-install.js
CRITICAL FIX: v7.1.0 Bun auto-install broken on Windows 11

Problem:
- hooks.json calls `bun smart-install.js` but if Bun isn't installed,
  command fails immediately with "bun is not recognized"
- smart-install.js never runs, so Bun never gets installed
- Chicken-and-egg problem

Root Cause:
- v7.1.0 removed Bun/uv auto-installation logic from smart-install.js
- Assumed Bun would already be available
- Breaks fresh installations on all platforms

Solution:
1. Changed SessionStart hook to use `node` for smart-install.js
   (Node.js always available in Claude Code)
2. Restored Bun auto-installation logic:
   - isBunInstalled() - check if Bun is in PATH
   - installBun() - auto-install via PowerShell (Windows) or curl (Unix/macOS)
   - Also restored uv auto-installation for Chroma

After Fix:
- smart-install.js runs with node (always available)
- Detects if Bun is missing and auto-installs it
- Subsequent hooks use bun successfully
- Works on fresh Windows installations

Fixes: User report from Discord - Windows 11 'bun' is not recognized

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:33:03 -05:00
Alex Newman f41824fa59 fix: quote all CLAUDE_PLUGIN_ROOT paths in hooks.json for Windows usernames with spaces
Wraps all ${CLAUDE_PLUGIN_ROOT} variable expansions with quotes to handle
Windows paths containing spaces (e.g., C:\Users\John Doe\.claude\...).

Without quotes, the shell splits the path at spaces, causing Node.js to
interpret the first segment as JavaScript and throw SyntaxError: Unexpected
token ':'.

This fix updates the Bun migration (v7.1.0) to include proper quoting,
superseding PR #212 which was based on pre-migration code.

Fixes #212

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:28:19 -05:00
Alex Newman 80ba7633e5 docs: update CHANGELOG.md for localhost-only binding security fix 2025-12-12 22:18:32 -05:00
Alex Newman d14266d70a feat(security): default worker to localhost-only binding (#236)
BREAKING CHANGE: Worker now binds to 127.0.0.1 by default instead of 0.0.0.0

This fixes a critical security issue where the worker service was exposing
all API endpoints to the network without authentication.

Changes:
- Default worker binding changed from 0.0.0.0 to 127.0.0.1 (localhost-only)
- Added CLAUDE_MEM_WORKER_HOST configuration setting
- Added host validation in settings API
- Updated UI to allow configuring bind address
- Updated documentation in README.md and CLAUDE.md

For users who need remote access (e.g., server deployments),
set CLAUDE_MEM_WORKER_HOST=0.0.0.0 in ~/.claude-mem/settings.json

All 42 tests pass. Build successful.

Co-authored-by: 7Sageer <12210216@mail.sustech.edu.cn>
2025-12-12 22:17:56 -05:00
Alex Newman 1cd545c36c Merge main into feature/localhost-only-binding - rebuild plugin files 2025-12-12 22:17:19 -05:00
Alex Newman 901af0b7f7 docs: update metadata and descriptions across multiple documentation files 2025-12-12 21:47:58 -05:00
Alex Newman 6815cc55b8 docs: add Mintlify documentation section to CLAUDE.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 21:26:51 -05:00
Alex Newman 12603a1a5c docs: add PM2 to Bun migration documentation to Mintlify
Add comprehensive technical documentation explaining the v7.1.0 migration from PM2 to Bun-based process management and better-sqlite3 to bun:sqlite database driver.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 21:22:00 -05:00
Alex Newman 15d05b5ac7 docs: update CHANGELOG.md for v7.1.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 21:13:23 -05:00
Alex Newman bf4a20223a Merge feature/bun-instead-of-pm2: PM2 to Bun migration v7.1.0
Major architectural migration from PM2 to native Bun process management,
and better-sqlite3 to bun:sqlite database driver.

Key changes:
- Replace PM2 with custom Bun-based ProcessManager
- Migrate from better-sqlite3 npm to bun:sqlite runtime
- Auto-install Bun and uv (Python) on first run
- Automatic legacy PM2 process cleanup on all platforms
- Complete documentation in docs/PM2-TO-BUN-MIGRATION.md

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 21:12:34 -05:00
Alex Newman a549d9fe47 chore: update project files for Bun integration and streamline dependencies 2025-12-12 21:10:58 -05:00
Alex Newman e896cfa0c5 feat: add support for uv package manager installation and update documentation 2025-12-12 20:49:35 -05:00
Alex Newman 5d4e71d2ff fix: run PM2 cleanup on all platforms for quality migration
Previously skipped Windows PM2 cleanup based on assumption that PM2
wasn't used on Windows. However, this left potential orphaned processes.

Changes:
- Remove platform check (process.platform !== 'win32')
- Run PM2 cleanup on Mac/Linux/Windows consistently
- Create .pm2-migrated marker on all platforms
- Update documentation to reflect cross-platform behavior

Rationale:
- Quality migration should clean up ALL orphaned processes
- Error handling (try/catch) already makes this safe
- Even if PM2 had Windows issues, cleanup won't hurt
- Consistent behavior across platforms is better UX

Impact:
- Windows users will get PM2 cleanup on first hook trigger
- Marker file now created on Windows (prevents repeated attempts)
- No breaking changes (errors caught and ignored)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 20:16:39 -05:00
Alex Newman f923c0cdd5 fix: complete better-sqlite3 to bun:sqlite migration
Must Fix:
- Remove better-sqlite3 logic from smart-install.js (5 sections)
- Update all documentation to reference bun:sqlite (7 files)

Should Fix:
- Add defensive break statement in worker-cli.ts:38

Nice to Have:
- Add port validation in ProcessManager.start() (1024-65535)
- Add one-time marker for PM2 cleanup migration
- Verify clearPortCache() wiring (already correct)

Addresses PR #248 review feedback (comment #3648517713)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 19:23:35 -05:00
Alex Newman 1491123706 feat(ProcessManager): add Bun availability check and improve PID file validation
- Implemented a method to check if Bun is available in the system PATH.
- Updated the startWithBun method to return an error if Bun is not found.
- Enhanced PID file parsing to validate required fields and their types.
- Cleaned up stale PID files if the process is no longer alive.

fix(SettingsRoutes): clear port cache after updating settings

- Added a call to clearPortCache after writing updated settings to ensure the application uses the latest configuration.
2025-12-12 17:48:41 -05:00
Alex Newman 9f1745bdec Remove Korean and Chinese README files to streamline documentation 2025-12-12 17:31:08 -05:00
Alex Newman d25b1d7394 Add responsive Trendshift badges for dark and light themes
- Updated README.md to include responsive badges that adapt to the user's color scheme preference.
- Added new SVG files for dark and light theme badges to enhance visual consistency.
2025-12-12 17:12:39 -05:00
Alex Newman f6351434ae Update README.md to simplify Trendshift badge display 2025-12-12 17:10:12 -05:00
Alex Newman ef9716eb5c Update README.md to add support for dark mode Trendshift badge 2025-12-12 17:08:21 -05:00
Alex Newman 6ebb678306 Add translation cache and update README with Trendshift badge 2025-12-12 17:05:39 -05:00
Alex Newman 25684ea8f7 Add Chinese translation for README and implement README translation script
- Created README.zh.md for Chinese localization of the project.
- Developed a README translation script to support multiple languages using Claude Agent SDK.
- Implemented CLI and programmatic usage for the translation tool.
- Added examples for integrating the translation tool into build scripts and CI/CD pipelines.
- Enhanced error handling and logging for translation processes.
- Included support for various languages and output customization options.
2025-12-12 00:58:36 -05:00
Alex Newman 147b2c5aa5 Update CHANGELOG.md for v7.0.11: Add feature/bun-executable to experimental branch selector 2025-12-12 00:17:22 -05:00
Alex Newman f154e32145 feat: add mem-search skill and Claude Desktop integration documentation 2025-12-12 00:09:25 -05:00
Copilot 013fe9423e Release v7.0.11: Add feature/bun-executable to branch selector (#247)
* Initial plan

* feat: add feature/bun-executable to branch selector

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* docs: add branch switching validation and tests

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Release v7.0.11: Add feature/bun-executable to branch selector

Enable users to test feature/bun-executable branch via Settings UI.

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

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

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-12-11 20:30:23 -05:00
claude[bot] f50a005cef feat: add feature/bun-executable to branch selector
Add feature/bun-executable to the allowed branches list in the Version Channel selector, enabling users to switch to this branch from the settings UI.

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

Co-Authored-By: Alex Newman <thedotmack@users.noreply.github.com>
2025-12-11 23:33:30 +00:00
Alex Newman d24d5dda04 refactor: require Bun globally, add auto-install, remove Windows executable approach (#243)
- Delete BinaryManager.ts - no longer needed
- Simplify ProcessManager.ts - single Bun spawn path for all platforms
- Update smart-install.js - auto-install Bun if missing (Windows/Unix)
- Update documentation to reflect Bun requirement

This simplifies the codebase by:
- Using Bun consistently across all platforms (hooks + worker)
- Eliminating binary download/hosting complexity
- Zero native dependencies with bun:sqlite
- Auto-installing Bun on first run if not present

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 17:27:18 -05:00
Alex Newman 50c7603a37 refactor: update documentation and remove PM2 ecosystem configurations for Bun integration 2025-12-11 15:15:30 -05:00
Alex Newman 807d1d6100 feat: migrate scripts to Bun runtime
- Updated shebangs in user-message-hook.js, worker-cli.js, and worker-service.cjs to use Bun instead of Node.
- Modified build-hooks.js to generate Bun-compatible shebangs in built scripts.
- Enhanced sync-marketplace.cjs to trigger a worker restart after syncing files via an HTTP request.
- Improved worker-cli.ts to exit with appropriate status codes after executing commands.
- Added build-worker-binary.js to create a Windows executable for the worker service using Bun's compile feature.
2025-12-11 14:59:44 -05:00
Alex Newman ded9671a82 Refactor worker port handling and improve logging
- Replaced hardcoded migration port with dynamic port retrieval using `getWorkerPort()` in worker-cli.ts.
- Updated context generator to clarify error handling comments.
- Introduced timeout constants in ProcessManager for better maintainability.
- Configured SQLite settings using constants for mmap size and cache size in DatabaseManager.
- Added timeout constants for Git and NPM commands in BranchManager.
- Enhanced error logging in FormattingService and SearchManager to provide more context on failures.
- Removed deprecated silentDebug function and replaced its usage with logger.debug.
- Updated tests to use dynamic worker port retrieval instead of hardcoded values.
2025-12-11 14:49:47 -05:00
7Sageer b8a9f366e7 feat(security): default worker to localhost-only binding
BREAKING: Worker now binds to 127.0.0.1 by default.
Set CLAUDE_MEM_WORKER_HOST=0.0.0.0 for remote access.
2025-12-11 22:01:31 +08:00
Alex Newman 83b0f9551b feat: add admin endpoints for process management and improve error handling
- Introduced `/api/admin/restart` and `/api/admin/shutdown` endpoints in WorkerService for restarting and shutting down the service.
- Updated error message in hook-error-handler to provide clearer instructions for restarting the worker.
- Refactored worker-utils to remove PM2 dependency and implement ProcessManager for starting the worker service.
- Cleaned up legacy PM2 references and provided new manual start instructions.
2025-12-10 23:46:17 -05:00
Alex Newman 8bf22b3dc5 feat: implement worker CLI and process management for bun integration 2025-12-10 23:14:56 -05:00
Alex Newman f8108047c4 refactor: update hooks to use bun instead of node for script execution 2025-12-10 22:25:03 -05:00
Alex Newman e4bd0ae461 refactor: migrate from better-sqlite3 to bun:sqlite
- Updated build-hooks.js to remove better-sqlite3 dependency and use bun:sqlite.
- Modified smart-install.js to eliminate checks and installations related to better-sqlite3.
- Refactored Database.ts, SessionSearch.ts, SessionStore.ts, and migrations.ts to import and utilize bun:sqlite.
- Replaced exec and pragma calls with appropriate run methods for bun:sqlite compatibility.
- Removed unnecessary native module verification and installation logic for better-sqlite3.
2025-12-10 22:11:55 -05:00
Alex Newman 5b338ba34e Fix project filter and update export/import docs
Critical bug fix:
- Pass project filter to getSessionSummariesByIds() and getUserPromptsByIds() in SearchManager
- Previously only observations were filtered by project, sessions and prompts leaked from other projects

Documentation improvements:
- Update "FTS5 search" to "hybrid search" (accurate terminology)
- Add privacy warning about sensitive data in exports
- Document --project parameter for filtered exports
- Add "Export by Project" examples to advanced usage

Verified with test export using --project=claude-mem filter.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 20:38:21 -05:00
Alex Newman 4e7ed75fa9 Fix critical bugs in export/import feature (PR #225)
Addressed all 6 bugs identified in code reviews:

CRITICAL FIXES:
1. SessionStore.ts: Fixed concepts filter bug - removed empty params.push()
   that was breaking SQL parameter alignment (line 849)

2. import-memories.ts: Removed worker_port and prompt_counter fields from
   sdk_sessions insert to fix schema mismatch with fresh databases

3. export-memories.ts: Fixed hardcoded port - now reads from settings via
   SettingsDefaultsManager.loadFromFile()

HIGH PRIORITY:
4. export-memories.ts: Added database existence check with clear error
   message before opening database connection

5. export-memories.ts: Fixed variable shadowing - renamed local 'query'
   variable to 'sessionQuery' (line 90)

MEDIUM PRIORITY:
6. export-memories.ts: Improved type safety - added ObservationRecord,
   SdkSessionRecord, SessionSummaryRecord, UserPromptRecord interfaces

All fixes tested and verified:
- Export script successfully exports with project filtering
- Import script works on existing database with duplicate prevention
- Port configuration read from settings.json
- Type safety improvements prevent compile-time errors

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 20:15:26 -05:00
Alex Newman a8b84fa7b6 Update export script and rebuild with latest changes 2025-12-10 18:05:36 -05:00
Alex Newman 73be8f7a63 Fix export/import feature: JSON format and project filtering
Two critical fixes for the memory export/import feature:

1. **SearchManager empty results bug**: The /api/search endpoint
   with format=json now returns {observations: [], sessions: [],
   prompts: []} when no results are found, instead of the MCP
   protocol format. This fixes the export-memories script which
   expects consistent JSON structure regardless of result count.

2. **Project filtering support**: Updated SessionStore methods
   (getObservationsByIds, getSessionSummariesByIds, getUserPromptsByIds)
   to accept and apply project filter parameter. This enables
   proper project-based filtering during ChromaDB hybrid search
   result hydration.

Testing:
- Export with results:  50 observations exported
- Export with empty results:  Proper JSON structure
- Round-trip import:  Duplicate prevention working
- Project filtering:  claude-mem (51 obs) vs rad-mem (1 obs)

Fixes export/import feature blocking bugs.
2025-12-10 18:04:49 -05:00
Alex Newman fa93f2c1e2 Add export/import functionality and documentation for memory management 2025-12-10 17:39:55 -05:00
Alex Newman b39cf84730 Bump version to 7.0.10
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 17:22:56 -05:00
Alex Newman 772e235e92 Bump version to 7.0.9 and update CHANGELOG.md 2025-12-10 17:20:34 -05:00
Alex Newman 0986301e7a Update CHANGELOG.md for v7.0.9
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 16:29:51 -05:00
Alex Newman 822cf796e1 Bump version to 7.0.9
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 16:29:00 -05:00
7Sageer 53314d9c38 Fix: Return complete MCP response object instead of content array in SearchRoutes (#223)
All search route handlers were returning only result.content (array) instead
of the complete result object containing {content, isError}. This caused the
MCP server to receive an invalid response format, resulting in all search
tools hanging indefinitely.

Changes:
- Updated 14 route handlers to return full result object
- Ensures MCP protocol compatibility by providing expected response structure

Fixes search functionality for all claude-mem search tools including:
- Unified search, timeline, decisions, changes
- Observations, sessions, prompts search
- Concept, file, type filtering
- Recent context and timeline queries
2025-12-10 16:28:06 -05:00
Alex Newman 3a1ed0d299 Update CHANGELOG.md for v7.0.8 2025-12-10 14:26:45 -05:00
Alex Newman 8c589b6265 Bump version to 7.0.8 2025-12-10 14:25:51 -05:00
Alex Newman 8bdec6abc0 Fix critical session persistence bug in new-hook (#224)
* Fix critical session persistence bug in new-hook

Restores the second POST call to /sessions/:sessionDbId/init that was
incorrectly removed as "duplicate" in the hooks refactor.

The two POST calls serve different purposes:
- POST /api/sessions/init: Creates DB session, saves prompt (no SDK agent)
- POST /sessions/:sessionDbId/init: Starts the SDK agent session

Without the second call, the SDK agent never started, causing:
- First prompts saved to DB but never processed
- Subsequent prompts queued but no agent running to consume them
- Each new prompt creating orphaned sessions instead of continuing

This fix restores proper session continuation across multiple prompts.

Fixes conversation fragmentation reported in production.

* updated agent-sdk

* Skip meta-observations for session-memory file operations

Added a check to skip meta-observations when file operations (Edit, Write, Read, NotebookEdit) are performed on session-memory files. If the file path includes 'session-memory', a debug log is generated and the response indicates that the observation was skipped.
2025-12-10 14:24:19 -05:00
Copilot 41fbb87aa0 Update documentation from claude-opus-4 to claude-opus-4-5 (#190)
* Initial plan

* Update all references from claude-opus-4 to claude-opus-4-5

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Use shorthand model names (haiku, sonnet, opus) throughout codebase

Updated model references to use shorthand names that automatically forward to latest versions:
- UI components (Sidebar, ContextSettingsModal)
- Documentation (configuration.mdx, worker-service.mdx)
- Build artifacts (viewer-bundle.js)

Shorthand names provide forward compatibility without requiring version-specific updates.

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
Co-authored-by: Alex Newman <thedotmack@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 23:07:48 -05:00
Alex Newman fb9cccd350 Update CHANGELOG.md for v7.0.7
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 22:47:41 -05:00
Alex Newman 9387418707 Bump version to 7.0.7
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 22:46:50 -05:00
Alex Newman eaba21329c Refactor hooks codebase: reduce complexity and improve maintainability (#204)
* refactor: Clean up hook-response and new-hook files

- Removed 'PreCompact' hook type and associated logic from hook-response.ts for improved type safety.
- Deleted extensive architecture comments in new-hook.ts to streamline code readability.
- Simplified debug logging in new-hook.ts to reduce verbosity.
- Enhanced ensureWorkerRunning function in worker-utils.ts with a final health check before throwing errors.
- Added a new documentation file outlining the hooks cleanup process and future improvements.

* Refactor cleanup and user message hooks

- Updated cleanup-hook.js to improve error handling and remove unnecessary input checks.
- Simplified user-message-hook.js by removing time-sensitive announcements and streamlining output.
- Enhanced logging functionality in both hooks for better debugging and clarity.

* Refactor error handling in hooks to use centralized error handler

- Introduced `handleWorkerError` function in `src/shared/hook-error-handler.ts` to manage worker-related errors.
- Updated `context-hook.ts`, `new-hook.ts`, `save-hook.ts`, and `summary-hook.ts` to utilize the new error handler, simplifying error management and improving code readability.
- Removed repetitive error handling logic from individual hooks, ensuring consistent user-friendly messages for connection issues.

* Refactor user-message and summary hooks to utilize shared transcript parser; introduce hook exit codes

- Moved user message extraction logic to a new shared module `transcript-parser.ts` for better code reuse.
- Updated `summary-hook.ts` to use the new `extractLastMessage` function for retrieving user and assistant messages.
- Replaced direct exit code usage in `user-message-hook.ts` with constants from `hook-constants.ts` for improved readability and maintainability.
- Added `HOOK_EXIT_CODES` to `hook-constants.ts` to standardize exit codes across hooks.

* Refactor hook input interfaces to enforce required fields

- Updated `SessionStartInput`, `UserPromptSubmitInput`, `PostToolUseInput`, and `StopInput` interfaces to require `session_id`, `transcript_path`, and `cwd` fields, ensuring better type safety and clarity in hook inputs.
- Removed optional index signatures from these interfaces to prevent unintended properties and improve code maintainability.
- Adjusted related hook implementations to align with the new interface definitions.

* Refactor save-hook to remove tool skipping logic; enhance summary-hook to handle spinner stopping with error logging; update SessionRoutes to load skip tools from settings; add CLAUDE_MEM_SKIP_TOOLS to SettingsDefaultsManager for configurable tool exclusion.

* Document CLAUDE_MEM_SKIP_TOOLS setting in public docs

Added documentation for the new CLAUDE_MEM_SKIP_TOOLS configuration
setting in response to PR review feedback. Users can now discover and
customize which tools are excluded from observations.

🤖 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-09 22:45:22 -05:00
Alex Newman 84c4d62812 Update CHANGELOG.md for v7.0.6
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 20:22:29 -05:00
Alex Newman 9e9ff20cba Bump version to 7.0.6
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 20:21:37 -05:00
Alex Newman bc28891bca Merge pull request #203 from CrystallDEV/fix/windows-terminal-spawning
fix(windows): hide terminal windows when spawning child processes
2025-12-09 20:19:17 -05:00
Alex Newman bafc86832c Update src/services/worker-service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 20:19:06 -05:00
Alex Newman b985579959 Update scripts/smart-install.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 18:42:19 -05:00
CrystallDEV 5f36d2bf9a fix(windows): hide terminal windows when spawning child processes 2025-12-10 00:15:14 +01:00
Alex Newman 65e5411c21 Bump version to 7.0.5 in package.json 2025-12-09 17:29:46 -05:00
Alex Newman 7a22144069 Update CHANGELOG.md for v7.0.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 16:29:50 -05:00
Alex Newman 1360195390 Bump version to 7.0.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 16:28:01 -05:00
Alex Newman 6b38be29fb Merge pull request #201 from thedotmack/bugfix/settings-and-new-hook
Settings centralization and new-hook HTTP refactor
2025-12-09 16:25:58 -05:00
Alex Newman f992251c32 Refactor user-message-hook.js and worker-utils.ts for improved logging and Windows compatibility
- Enhanced logging functionality in user-message-hook.js to include better formatting and error handling.
- Updated worker-utils.ts to escape single quotes in PowerShell commands and added checks for global PM2 installation.
- Improved readability and maintainability of the code by restructuring and clarifying variable names.
2025-12-09 16:21:29 -05:00
Alex Newman c2015c4dfc Fix circular dependency crash in worker service
**Problem:**
Worker service crashed on startup with:
  TypeError: Cannot read properties of undefined (reading 'get')
  at new Wd (.../worker-service.cjs:52:131469)

**Root Cause:**
Circular dependency between SettingsDefaultsManager and logger:
  1. SettingsDefaultsManager imports logger
  2. logger imports SettingsDefaultsManager
  3. logger constructor calls SettingsDefaultsManager.get() at init time
  4. When CommonJS resolves the cycle, SettingsDefaultsManager is undefined

**Solution:**
Break the circular dependency by making logger lazy-load its configuration:
  - Change logger.level from initialized in constructor to lazy-loaded
  - Add getLevel() method that loads on first access
  - Update all level checks to use getLevel()

This allows SettingsDefaultsManager to import logger without triggering
the circular dependency, since logger no longer accesses SettingsDefaultsManager
during module initialization.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 16:13:10 -05:00
Alex Newman 005a80c540 Refactor SettingsDefaultsManager: Move to shared directory and update imports
- Moved SettingsDefaultsManager from worker/settings to shared directory.
- Updated all import paths across the codebase to reflect the new location.
- Removed early-settings.ts as its functionality is now handled by SettingsDefaultsManager.
- Adjusted logger and paths to utilize SettingsDefaultsManager for configuration values.
2025-12-09 15:29:17 -05:00
Alex Newman c3761a2204 Refactor silent debugging to happy path error handling
- Replaced instances of silentDebug with happy_path_error__with_fallback across multiple files to improve error logging and handling.
- Updated the utility function to provide clearer semantics for error handling when expected values are missing.
- Introduced a script to find potential silent failures in the codebase that may need to be addressed with the new error handling approach.
2025-12-09 15:09:44 -05:00
Alex Newman d957bff495 Remove postinstall script and update user message for first-time setup in hooks 2025-12-09 14:39:31 -05:00
Alex Newman c5ee27f001 Fix postinstall script path for first-run completion 2025-12-09 14:35:57 -05:00
Alex Newman d9f3798c90 Refactor user message hook for first-run detection, update Python version regex validation in settings routes, and simplify package commands directory retrieval 2025-12-09 14:33:23 -05:00
Alex Newman 1fb8df42b6 Refactor hook timeout settings to use centralized constants
- Introduced a new module `hook-constants.ts` to define timeout constants for various hooks.
- Updated `cleanup-hook.ts`, `context-hook.ts`, `save-hook.ts`, and `summary-hook.ts` to utilize the new `HOOK_TIMEOUTS.DEFAULT` for fetch timeouts instead of hardcoded values.
- Adjusted worker utility timeouts in `worker-utils.ts` to use constants from `hook-constants.ts`, improving maintainability and consistency across the codebase.
2025-12-09 14:25:53 -05:00
Alex Newman e09e64ade5 Refactor context and new hooks to use fetch API instead of execSync for HTTP requests; improve error handling for worker connection issues 2025-12-09 14:10:59 -05:00
Alex Newman 7cab32151e Enhance error handling and logging in early-settings and worker-utils
- Added silent debugging for settings file loading failures in early-settings.ts.
- Improved error logging in worker-utils.ts for health check and worker startup failures, including detailed error information and context.
2025-12-09 14:04:32 -05:00
Alex Newman a2f7a4dc5a Refactor new-hook to initialize sessions via HTTP and improve privacy handling
- Removed direct database operations in new-hook.ts and replaced them with an HTTP call to initialize sessions.
- Added error handling for HTTP requests and improved logging for session initialization.
- Updated SessionRoutes to handle new session initialization and privacy checks.
- Enhanced privacy tag stripping logic to prevent saving fully private prompts.
- Improved overall error handling and debugging messages throughout the session management process.
2025-12-09 13:43:11 -05:00
Alex Newman fc5c2d5e07 Refactor settings management to use ~/.claude-mem/settings.json
- Updated paths in troubleshooting documentation to reflect new settings file location.
- Modified diagnostics and reference files to read from ~/.claude-mem/settings.json.
- Introduced getWorkerPort utility for cleaner worker port retrieval.
- Enhanced ChromaSync and SDKAgent to load Python version and Claude path from settings.
- Updated SettingsRoutes to validate new settings: CLAUDE_MEM_LOG_LEVEL and CLAUDE_MEM_PYTHON_VERSION.
- Added early-settings module to load settings for logger and other early-stage modules.
- Adjusted logger to use early-loaded log level setting.
- Refactored paths to utilize early-loaded data directory setting.
2025-12-09 12:23:33 -05:00
Alex Newman b22adcca05 refactor: update permissions in settings.json and remove deprecated settings script 2025-12-09 11:44:09 -05:00
Alex Newman 2adc830c71 docs: update CHANGELOG.md for v7.0.4 2025-12-09 11:17:57 -05:00
Alex Newman e2c8f6b99e Merge pull request #196 from thedotmack/claude/release-7.0.4-windows-fixes-012Ny54FxUuyiNdJ28p1ohwd
chore: bump version to 7.0.4
2025-12-09 10:32:49 -05:00
Claude 280608574b chore: bump version to 7.0.4
Comprehensive Windows bug fixes release. Thanks to @kat-bell for the
excellent contributions fixing Windows plugin installation and worker
startup issues.
2025-12-09 15:29:34 +00:00
Alex Newman 291f43d2c7 Merge pull request #195 from kat-bell/fix/windows-worker-startup-v2
fix(windows): Comprehensive fixes for Windows plugin installation
2025-12-09 10:24:22 -05:00
kat-bell d7dc29498c fix(cache): Add package.json to plugin directory for cache dependency resolution
The bundled hook scripts use `external: ['better-sqlite3']` during esbuild,
meaning the dependency must be resolved at runtime. When hooks run from the
cache directory (~/.claude/plugins/cache/thedotmack/claude-mem/X.X.X/),
they couldn't find better-sqlite3 because:

1. Cache directory had no package.json
2. smart-install.js was hardcoded to install in marketplace directory only

This fix:
- Adds plugin/package.json declaring runtime dependencies (better-sqlite3)
- Updates build-hooks.js to auto-generate plugin/package.json from main package.json
- Updates smart-install.js to detect execution context (cache vs marketplace)
  and install dependencies in the correct location

The script now detects if it's running from cache (via path pattern matching)
and installs dependencies there, where the hooks actually execute.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 05:27:26 -06:00
kat-bell 1f2e5f1a9c fix(windows): Comprehensive fixes for Windows plugin installation
This PR addresses issue #193 affecting Windows installations of claude-mem.

## Bug 1: Missing ecosystem.config.cjs in packaged plugin

**Problem**: The ecosystem.config.cjs file was not included in the plugin
package, causing PM2 to fail when trying to start the worker from cache.

**Fix**: Added `plugin/ecosystem.config.cjs` with correct path for packaged
structure (`./scripts/worker-service.cjs` instead of `./plugin/scripts/`).

## Bug 2: Incorrect MCP Server Path (src/services/worker-service.ts)

**Problem**: Path `__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs'`
only worked in dev structure, failed in packaged plugin.

**Error produced**:
```
Error: Cannot find module 'C:\Users\...\claude-mem\plugin\scripts\mcp-server.cjs'
[ERROR] [SYSTEM] Background initialization failed MCP error -32000: Connection closed
```

**Fix**: Changed to `path.join(__dirname, 'mcp-server.cjs')` since mcp-server.cjs
is in the same directory as worker-service.cjs after bundling.

## Bug 3: Missing smart-install.js in plugin package

**Problem**: smart-install.js was referenced in hooks.json but not included
in the plugin/ directory for cache deployment.

**Fix**: Added `plugin/scripts/smart-install.js` that uses `createRequire()`
to resolve modules from MARKETPLACE_ROOT.

## Bug 4: hooks.json incorrect path

**Problem**: Referenced `/../scripts/smart-install.js` but CLAUDE_PLUGIN_ROOT
points to the plugin/ directory.

**Fix**: Changed to `/scripts/smart-install.js`.

## Bug 5: Windows Worker Startup - Visible Console Windows

**Problem**: PM2 ignores windowsHide option on Windows, opening visible
console windows when starting the worker service.

**Fix**: Use PowerShell `Start-Process -WindowStyle Hidden` on Windows while
keeping PM2 for Unix systems (src/shared/worker-utils.ts).

## Additional Improvements

- Increased worker startup timeouts for Windows (500ms health check, 1000ms
  wait between retries, 15 retries = 15s total vs previous 5s)
- Added `windowsHide: true` to root ecosystem.config.cjs for PM2

## Note on Assertion Failure

The Windows libuv assertion failure `!(handle->flags & UV_HANDLE_CLOSING)`
at `src\win\async.c:76` is a known upstream issue in Claude Code (Issue #7579),
triggered by fetch() calls on Windows. This is NOT caused by worker spawning
and cannot be fixed in claude-mem.

Tested on Windows 11 with Node.js v24.

Fixes #193

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 05:02:56 -06:00
Alex Newman 679a077f9b docs: update CHANGELOG.md for v7.0.3 2025-12-09 01:08:34 -05:00
Alex Newman f7a80e6abc chore: bump version to 7.0.3
Complete search-server to mcp-server rename

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 01:07:41 -05:00
Alex Newman 4321add69c refactor: rename search-server to mcp-server throughout codebase
- Updated all documentation references from search-server to mcp-server
- Removed legacy search-server.cjs file
- Updated debug log messages to use [mcp-server] prefix
- Updated build output references in docs

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 01:06:43 -05:00
Alex Newman 105b4ca70d docs: update CHANGELOG.md for v7.0.2 2025-12-09 01:03:47 -05:00
Alex Newman 06ba1cd92c chore: bump version to 7.0.2
Auto-start worker functionality improvements

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 01:02:55 -05:00
Alex Newman b003a43e73 docs: update CHANGELOG.md for v7.0.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 00:53:58 -05:00
130 changed files with 8723 additions and 8740 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "7.0.1",
"version": "7.1.11",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+1
View File
@@ -0,0 +1 @@
github: thedotmack
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,131 @@
name: Convert Feature Requests to Discussions
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to convert to discussion'
required: true
type: number
jobs:
convert:
runs-on: ubuntu-latest
# Only run on labeled event if the label is 'feature-request', or always run on workflow_dispatch
if: |
(github.event_name == 'issues' && github.event.label.name == 'feature-request') ||
github.event_name == 'workflow_dispatch'
permissions:
issues: write
discussions: write
contents: read
steps:
- name: Get issue details and create discussion
id: discussion
uses: actions/github-script@v7
with:
script: |
// Get issue details
let issue;
if (context.eventName === 'workflow_dispatch') {
const { data } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.inputs.issue_number
});
issue = data;
} else {
issue = context.payload.issue;
}
console.log(`Processing issue #${issue.number}: ${issue.title}`);
// Format the discussion body with a reference to the original issue
const discussionBody = `> Originally posted as issue #${issue.number} by @${issue.user.login}\n> ${issue.html_url}\n\n${issue.body || 'No description provided.'}`;
const mutation = `
mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
createDiscussion(input: {
repositoryId: $repositoryId
categoryId: $categoryId
title: $title
body: $body
}) {
discussion {
url
number
}
}
}
`;
const variables = {
repositoryId: 'R_kgDOPng1Jw',
categoryId: 'DIC_kwDOPng1J84Cw86z',
title: issue.title,
body: discussionBody
};
try {
const result = await github.graphql(mutation, variables);
const discussionUrl = result.createDiscussion.discussion.url;
const discussionNumber = result.createDiscussion.discussion.number;
core.setOutput('url', discussionUrl);
core.setOutput('number', discussionNumber);
core.setOutput('issue_number', issue.number);
console.log(`Created discussion #${discussionNumber}: ${discussionUrl}`);
return { discussionUrl, discussionNumber, issueNumber: issue.number };
} catch (error) {
core.setFailed(`Failed to create discussion: ${error.message}`);
throw error;
}
- name: Comment on issue
uses: actions/github-script@v7
with:
script: |
const issueNumber = ${{ steps.discussion.outputs.issue_number }};
const discussionUrl = '${{ steps.discussion.outputs.url }}';
const comment = `This feature request has been moved to [Discussions](${discussionUrl}) to keep bug reports separate from feature ideas.\n\nPlease continue the conversation there - we'd love to hear your thoughts!`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: comment
});
console.log(`Added comment to issue #${issueNumber}`);
- name: Close and lock issue
uses: actions/github-script@v7
with:
script: |
const issueNumber = ${{ steps.discussion.outputs.issue_number }};
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed'
});
console.log(`Closed issue #${issueNumber}`);
// Lock the issue
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
lock_reason: 'resolved'
});
console.log(`Locked issue #${issueNumber}`);
+34
View File
@@ -0,0 +1,34 @@
name: Summarize new issues
on:
issues:
types: [opened]
jobs:
summary:
runs-on: ubuntu-latest
permissions:
issues: write
models: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run AI inference
id: inference
uses: actions/ai-inference@v1
with:
prompt: |
Summarize the following GitHub issue in one paragraph:
Title: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
- name: Comment with AI summary
run: |
gh issue comment $ISSUE_NUMBER --body '${{ steps.inference.outputs.response }}'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
RESPONSE: ${{ steps.inference.outputs.response }}
+4 -1
View File
@@ -14,4 +14,7 @@ package-lock.json
private/
# Generated UI files (built from viewer-template.html)
src/ui/viewer.html
src/ui/viewer.html
# Local MCP server config (for development only)
.mcp.json
+1 -12
View File
@@ -1,14 +1,3 @@
{
"mcpServers": {
"old-claude-mem": {
"command": "uvx",
"args": [
"chroma-mcp",
"--client-type",
"persistent",
"--data-dir",
"/Users/alexnewman/.claude-mem/backups/chroma-backup-20251005-222403"
]
}
}
"mcpServers": {}
}
+116
View File
@@ -0,0 +1,116 @@
{
"sourceHash": "9ab0d799179c66f9",
"lastUpdated": "2025-12-12T07:42:03.489Z",
"translations": {
"zh": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.12256679999999998
},
"ja": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.12973164999999998
},
"pt-br": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.11508539999999999
},
"ko": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.13952664999999997
},
"es": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.12530165
},
"de": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.12232164999999998
},
"fr": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:06:55.026Z",
"costUsd": 0.11906665
},
"he": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.151329
},
"ar": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.151952
},
"ru": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.13418499999999997
},
"pl": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.13196799999999997
},
"cs": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.12714599999999998
},
"nl": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.118389
},
"tr": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.13991199999999998
},
"uk": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:25:00.076Z",
"costUsd": 0.13786
},
"vi": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.15467299999999998
},
"id": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.185581
},
"th": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.177859
},
"hi": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.17412700000000003
},
"bn": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.202735
},
"ro": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.12212875
},
"sv": {
"hash": "9ab0d799179c66f9",
"translatedAt": "2025-12-12T07:42:03.489Z",
"costUsd": 0.12143675000000001
}
}
}
+167
View File
@@ -0,0 +1,167 @@
# Action Plan: Issues & PRs Cleanup
Generated: 2025-12-12
## Phase 1: Immediate Cleanup (Today)
### Close Obsolete PRs
- [ ] **#255** - Close PR "Fix PM2 worker MODULE_NOT_FOUND"
- Reason: v7.1.0 removed PM2 entirely, this fix is no longer relevant
- Comment: Explain that v7.1.0 migration to Bun eliminated PM2 dependency
- [ ] **#206** - Close or request update on "Harden worker startup"
- Reason: Contains PM2-specific code that no longer exists
- Comment: Ask author if they want to update for Bun architecture, otherwise close as obsolete
### Close/Update Fixed Issues
- [ ] **#213** - Comment and close "Windows endless process spawning"
- Reason: v7.1.0 Bun migration eliminated PM2 process management
- Comment: Ask user to verify fix on v7.1.0, explain PM2 removal resolved issue
- [ ] **#229** - Close as duplicate
- Reason: Duplicate of #227 (upstream Claude Code bug)
- Comment: Direct to #227 for full details and workaround
- [ ] **#211** - Answer and close "Cursor IDE support question"
- Reason: Product question, not a bug report
- Comment: Explain focus is Claude Code, but plugin architecture may allow future expansion
### Critical Bug Follow-Up
- [ ] **#254** - Follow up on "Worker API fetch failed"
- Current status: Asked about PM2 logs (pre-v7.1.0 comment)
- Action: Update comment asking:
- What version of claude-mem are you running?
- If pre-v7.1.0: Please upgrade to v7.1.0 which fixes PM2 issues
- If v7.1.0+: Run troubleshoot skill and share logs
## Phase 2: High-Priority Merges (This Week)
### Security & Critical Fixes
- [ ] **#236** - Review and merge "Localhost-only binding" 🔒 PRIORITY
- Impact: Security improvement (fixes network exposure)
- Status: 156 additions, all tests pass (42/42)
- Action: Final review, merge, update CHANGELOG
- [ ] **#212** - Review and merge "Windows path quoting fix"
- Impact: Fixes Windows usernames with spaces
- Status: 6 lines changed, minimal risk
- Action: Quick cross-platform test, merge
### Major Features (Maintainer-Authored)
- [ ] **#225** - Review and merge "Export/Import scripts"
- Impact: Enables backup/restore, partially addresses #233
- Status: 927 additions, extensively tested by maintainer
- Action: Final review, merge, update docs
- [ ] **#250** - Review and merge "README translations"
- Impact: International user onboarding (22 languages)
- Status: 10,209 additions (massive but low-risk)
- Action: Spot-check a few translations, merge
### User-Requested Features
- [ ] **#252** - Test and merge "Execution traces" (addresses #194)
- Impact: Shows tools/skills/MCPs in UI bubbles
- Status: 383 additions, comprehensive implementation
- Action: Test database migration, API endpoints, UI display
- [ ] **#251** - Test and merge "Plan file context" (addresses #180)
- Impact: Injects last plan file into context
- Status: 85 additions, follows existing patterns
- Action: Test with real plan files, verify toggle works
## Phase 3: Review & Consider (Next Week)
### Quality Enhancements
- [ ] **#230** - Review "Multi-language support" (addresses #228)
- Impact: Observations/summaries in user's language
- Status: 157 additions, Korean screenshot provided
- Action: Review prompt changes carefully, test with multiple languages
- [ ] **#226** - Review "CLAUDE_CONFIG_DIR support"
- Impact: Supports non-standard Claude installations
- Status: 10 additions, minimal change
- Action: Test with custom config directory, merge if working
### Developer Experience
- [ ] **#216** - Review "Makefile shortcuts"
- Impact: DX improvement for contributors
- Status: 1,085 additions
- Priority: Low (not urgent)
- Action: Review when time permits
## Phase 4: Issue Follow-Ups (Ongoing)
### Awaiting User Verification
- [ ] **#209** - Follow up if no response on Windows worker startup
- Status: Already commented asking for v7.1.0 verification
- Action: Close if verified fixed, or investigate if still broken
- [ ] **#231** - Follow up if no response on module resolution
- Status: Already commented asking for v7.1.0 verification
- Action: Close if verified fixed, or investigate if still broken
### Upstream Bugs (Keep Open)
- [ ] **#227** - Keep open as documented upstream bug
- Reason: Claude Code CLI uses invalid Windows paths
- Action: No action needed, workaround documented
### Active Bugs (Investigate)
- [ ] **#208** - Investigate "Windows console windows appearing"
- Priority: Medium (cosmetic but annoying)
- Action: Reproduce on Windows, identify root cause
## Phase 5: Future Feature Planning
### Feature Requests Without PRs
- [ ] **#240** - Plan "Move MCP scaffolding to separate file"
- Type: Internal refactoring
- Priority: Low
- Action: Design approach when time permits
- [ ] **#239** - Plan "Track git branch as metadata"
- Type: Context enhancement
- Priority: Medium
- Action: Design schema changes, discuss approach
- [ ] **#215** - Plan "PreCompact event hook"
- Type: Power user feature
- Priority: Low
- Action: Evaluate use cases, design API
- [ ] **#233** - Plan "Multi-device sync" (partial solution exists)
- Type: Major feature
- Note: PR #225 provides export/import, full sync is more complex
- Action: Determine if export/import is sufficient, or plan cloud sync
## Summary
### Quick Wins (Do Today)
- Close 2 obsolete PRs (#255, #206)
- Close 3 resolved/duplicate issues (#213, #229, #211)
- Follow up on critical bug (#254)
### High-Impact Merges (This Week)
- Merge security fix (#236)
- Merge 2 simple fixes (#212, #225)
- Merge 2 major features (#250, #252, #251)
### Expected Impact
- **Security**: Localhost-only by default
- **Functionality**: Export/import, execution traces, plan context
- **UX**: Multi-language support, Windows fixes
- **Clarity**: Clean backlog, remove PM2 confusion
---
**Next Review**: After Phase 2 completion, reassess remaining items
+364
View File
@@ -4,6 +4,370 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [7.1.10] - 2025-12-14
## Enhancement
This release adds automatic orphan cleanup to complement the process leak fix from v7.1.9.
### Added
- **Auto-Cleanup on Startup**: Worker now automatically detects and kills orphaned chroma-mcp processes before starting
- Scans for existing chroma-mcp processes on worker startup
- Kills all found processes before creating new ones
- Logs cleanup activity (process count and PIDs)
- Non-fatal error handling (continues on cleanup failure)
### Benefits
- Automatically recovers from pre-7.1.9 process leaks without manual intervention
- Ensures clean slate on every worker restart
- Prevents accumulation even if v7.1.9's close() method fails
- No user action required - works transparently
### Example Logs
```
[INFO] [SYSTEM] Cleaning up orphaned chroma-mcp processes {count=2, pids=33753,33750}
[INFO] [SYSTEM] Orphaned processes cleaned up {count=2}
```
### Recommendation
Upgrade from v7.1.9 to get automatic orphan cleanup. Combined with v7.1.9's proper subprocess cleanup, this provides comprehensive protection against process leaks.
---
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.9...v7.1.10
## [7.1.9] - 2025-12-14
## Critical Bugfix
This patch release fixes a critical memory leak that caused chroma-mcp processes to accumulate with each worker restart, leading to memory exhaustion and silent backfill failures.
### Fixed
- **Process Leak Prevention**: ChromaSync now properly cleans up chroma-mcp subprocesses when the worker is restarted
- Store reference to StdioClientTransport subprocess
- Explicitly close transport to kill subprocess on shutdown
- Add error handling to ensure cleanup even on failures
- Reset all state in finally block
### Impact
- Eliminates process accumulation (16+ orphaned processes seen in production)
- Prevents memory exhaustion from leaked subprocesses (900MB+ RAM usage)
- Fixes silent backfill failures caused by OOM kills
- Ensures graceful cleanup on worker shutdown
### Recommendation
**All users should upgrade immediately** to prevent memory leaks and ensure reliable backfill operation.
---
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.8...v7.1.9
## [7.1.8] - 2025-12-13
## Memory Export/Import Scripts
Added portable memory export and import functionality with automatic duplicate prevention.
### New Features
- **Export memories** to JSON format with search filtering and project-based filtering
- **Import memories** with automatic duplicate detection via composite keys
- Complete documentation in docs/public/usage/export-import.mdx
### Use Cases
- Share memory sets between developers working on the same project
- Backup and restore specific project memories
- Collaborate on domain knowledge across teams
- Migrate memories between different claude-mem installations
### Example Usage
```bash
# Export Windows-related memories
npx tsx scripts/export-memories.ts "windows" windows-work.json
# Export only claude-mem project memories
npx tsx scripts/export-memories.ts "bugfix" fixes.json --project=claude-mem
# Import memories (with automatic duplicate prevention)
npx tsx scripts/import-memories.ts windows-work.json
```
### Technical Improvements
- Fixed JSON format response in /api/search endpoint for consistent structure
- Enhanced project filtering in ChromaDB hybrid search result hydration
- Duplicate detection using composite keys (session ID + title + timestamp)
## [7.1.7] - 2025-12-13
## Fixed
- Removed Windows workaround that was causing libuv assertion failures
- Prioritized stability over cosmetic console window issue
## Known Issue
- On Windows, a console window may briefly appear when the worker starts (cosmetic only, does not affect functionality)
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.6...v7.1.7
## [7.1.6] - 2025-12-13
## What's Changed
Improved error messages with platform-specific worker restart instructions for better troubleshooting experience.
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.5...v7.1.6
## [7.1.5] - 2025-12-13
## What's Changed
* fix: Use getWorkerHost() instead of hardcoded localhost in MCP server (#276)
### Bug Fix
Fixes Windows IPv6 issue where `localhost` resolves to `::1` (IPv6) but worker binds to `127.0.0.1` (IPv4), causing MCP tool connections to fail.
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.4...v7.1.5
## [7.1.4] - 2025-12-13
## What's Changed
* fix: add npm fallback when bun install fails with alias packages (#265)
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.3...v7.1.4
## [7.1.3] - 2025-12-13
## Bug Fixes
### Smart Install Script Refactoring
Refactored the smart-install.js script to improve code quality and maintainability:
- Extracted common installation paths as top-level constants (BUN_COMMON_PATHS, UV_COMMON_PATHS)
- Simplified installation check functions to delegate to dedicated path-finding helpers
- Streamlined installation verification logic with clearer error messages
- Removed redundant post-installation verification checks
- Improved error propagation by removing unnecessary retry logic
This refactoring reduces code duplication and makes the installation process more maintainable while preserving the same functionality for detecting Bun and uv binaries across platforms.
## [7.1.2] - 2025-12-13
## 🐛 Bug Fixes
### Windows Installation
- Fixed Bun PATH detection on Windows after fresh install
- Added fallback to check common install paths before PATH reload
- Improved smart-install.js to use full Bun path when not in PATH
- Added proper path quoting for Windows usernames with spaces
### Worker Startup
- Fixed worker connection failures in Stop hook
- Added health check retry loop (5 attempts, 500ms intervals)
- Worker now waits up to 2.5s for responsiveness before returning
- Improved error detection for Bun's ConnectionRefused error format
---
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.1...v7.1.2
## [7.1.1] - 2025-12-13
## 🚨 Critical Fixes
### Windows 11 Bun Auto-Install Fixed
- **Problem**: v7.1.0 had a chicken-and-egg bug where `bun smart-install.js` failed if Bun wasn't installed
- **Solution**: SessionStart hook now uses `node` (always available) for smart-install.js
- **Impact**: Fresh Windows installations now work out-of-box
### Path Quoting for Windows
- Fixed `hooks.json` to quote all paths
- Prevents SyntaxError for usernames with spaces (e.g., "C:\Users\John Doe\")
## ✨ New Feature
### Automatic Worker Restart on Version Updates
- Worker now automatically restarts when plugin version changes
- No more manual `npm run worker:restart` needed after upgrades
- Eliminates connection errors from running old worker code
## 📝 Notes
- **No manual actions required** - worker auto-restarts on next session start
- All future upgrades will automatically restart the worker
- Fresh installs on Windows 11 work correctly
## 🔗 Links
- [Full Changelog](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md#711---2025-12-12)
- [Documentation](https://docs.claude-mem.ai)
## [7.1.0] - 2025-12-13
## Major Architectural Migration
This release completely replaces PM2 with native Bun-based process management and migrates from better-sqlite3 to bun:sqlite.
### Key Changes
**Process Management**
- Replace PM2 with custom Bun-based ProcessManager
- PID file-based process tracking
- Automatic legacy PM2 process cleanup on all platforms
**Database Driver**
- Migrate from better-sqlite3 npm package to bun:sqlite runtime module
- Zero native compilation required
- Same API compatibility
**Auto-Installation**
- Bun runtime auto-installed if missing
- uv (Python package manager) auto-installed for Chroma vector search
- Smart installer with platform-specific methods (curl/PowerShell)
### Migration
**Automatic**: First hook trigger after update performs one-time PM2 cleanup and transitions to new architecture. No user action required.
### Documentation
Complete technical documentation in `docs/PM2-TO-BUN-MIGRATION.md`
## [7.0.11] - 2025-12-12
Patch release adding feature/bun-executable to experimental branch selector for testing Bun runtime integration.
## [7.0.9] - 2025-12-10
## Bug Fixes
- Fixed MCP response format in search route handlers - all 14 search endpoints now return complete response objects with error status instead of just content arrays, restoring MCP protocol compatibility
## Changes
- `SearchRoutes.ts`: Updated all route handlers to return full result object instead of extracted content property
## [7.0.8] - 2025-12-10
## Bug Fixes
- **Critical**: Filter out meta-observations for session-memory files to prevent recursive timeline pollution
- Memory agent was creating observations about editing Agent SDK's session-memory/summary.md files
- This created a recursive loop where investigating timeline pollution caused more pollution
- Filter now skips Edit/Write/Read/NotebookEdit operations on any file path containing 'session-memory'
- Eliminates 91+ meta-observations that were polluting the timeline
## Technical Details
Added filtering logic in SessionRoutes.ts to detect and skip file operations on session-memory files before observations are queued to the SDK agent. This prevents the memory agent from observing its own observation metadata files.
## [7.0.7] - 2025-12-10
## What's Changed
### Code Quality Improvements
- Refactored hooks codebase to reduce complexity and improve maintainability (#204)
- Net reduction of 78 lines while adding new functionality
- Improved type safety across all hook input interfaces
### New Features
- Added `CLAUDE_MEM_SKIP_TOOLS` configuration setting for controlling which tools are excluded from observations
- Default skip tools: `ListMcpResourcesTool`, `SlashCommand`, `Skill`, `TodoWrite`, `AskUserQuestion`
### Technical Improvements
- Created shared utilities: `transcript-parser.ts`, `hook-constants.ts`, `hook-error-handler.ts`
- Migrated business logic from hooks to worker service for better separation of concerns
- Enhanced error handling and spinner management
- Removed dead code and unnecessary abstractions
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.0.6...v7.0.7
## [7.0.6] - 2025-12-10
## Bug Fixes
- Fixed Windows terminal spawning to hide terminal windows when spawning child processes (#203, thanks @CrystallDEV)
- Improved worker service process management on Windows
## Contributors
Thanks to @CrystallDEV for this contribution!
## [7.0.5] - 2025-12-09
## What's Changed
### Bug Fixes
- Fixed settings schema inconsistency between write and read operations
- Fixed PowerShell command injection vulnerability in worker-utils.ts
- Enhanced PM2 existence check with clear error messages
- Added error logging to silent tool serialization handlers
### Improvements
- Settings centralization: Migrated to SettingsDefaultsManager across codebase
- Auto-creation of settings.json file with defaults on first run
- Settings schema migration from nested to flat format
- Refactored HTTP-only new-hook implementation
- Cross-platform worker service improvements
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.0.4...v7.0.5
## [7.0.4] - 2025-12-09
## What's Changed
### Bug Fixes
- **Windows**: Comprehensive fixes for Windows plugin installation
- **Cache**: Add package.json to plugin directory for cache dependency resolution
Thanks to @kat-bell for the excellent contributions!
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.0.3...v7.0.4
## [7.0.3] - 2025-12-09
## What's Changed
**Refactoring:**
- Completed rename of `search-server` to `mcp-server` throughout codebase
- Updated all documentation references from search-server to mcp-server
- Updated debug log messages to use `[mcp-server]` prefix
- Removed legacy `search-server.cjs` file
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.0.2...v7.0.3
## [7.0.2] - 2025-12-09
## What's Changed
**Bug Fixes:**
- Improved auto-start worker functionality for better reliability
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.0.1...v7.0.2
## [7.0.1] - 2025-12-09
## Bug Fixes
- **Hook Execution**: Ensure worker is running at the beginning of all hook files
- **Context Hook**: Replace waitForPort with ensureWorkerRunning for better error handling
- **Reliability**: Move ensureWorkerRunning to start of all hook functions to ensure worker is started before any logic executes
## Technical Changes
- context-hook.ts: Replace waitForPort logic with ensureWorkerRunning
- summary-hook.ts: Move ensureWorkerRunning before input validation
- new-hook.ts: Move ensureWorkerRunning before debug logging
- save-hook.ts: Move ensureWorkerRunning before SKIP_TOOLS check
- cleanup-hook.ts: Move ensureWorkerRunning before silentDebug calls
This ensures more reliable worker startup and clearer error messages when the worker fails to start.
## [7.0.0] - 2025-12-08
# Major Architectural Refactor
+36 -6
View File
@@ -6,7 +6,7 @@
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
**Current Version**: 7.0.1
**Current Version**: 7.1.11
## Architecture
@@ -14,7 +14,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
**Hooks** (`src/hooks/*.ts`) - TypeScript → ESM, built to `plugin/scripts/*-hook.js`
**Worker Service** (`src/services/worker-service.ts`) - Express API on port 37777, PM2-managed, handles AI processing asynchronously
**Worker Service** (`src/services/worker-service.ts`) - Express API on port 37777, Bun-managed, handles AI processing asynchronously
**Database** (`src/services/sqlite/`) - SQLite3 at `~/.claude-mem/claude-mem.db` with FTS5 full-text search
@@ -42,12 +42,29 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
**Viewer UI**: `npm run build && npm run sync-marketplace && npm run worker:restart`
## Environment Variables
## Configuration
Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created with defaults on first run.
**Core Settings:**
- `CLAUDE_MEM_MODEL` - Model for observations/summaries (default: claude-haiku-4-5)
- `CLAUDE_MEM_CONTEXT_OBSERVATIONS` - Observations injected at SessionStart (default: 50)
- `CLAUDE_MEM_WORKER_PORT` - Worker service port (default: 37777)
- `CLAUDE_MEM_WORKER_HOST` - Worker bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)
**System Configuration:**
- `CLAUDE_MEM_DATA_DIR` - Data directory location (default: ~/.claude-mem)
- `CLAUDE_MEM_LOG_LEVEL` - Log verbosity: DEBUG, INFO, WARN, ERROR, SILENT (default: INFO)
- `CLAUDE_MEM_PYTHON_VERSION` - Python version for uvx/chroma-mcp (default: 3.13, avoids onnxruntime compatibility issues with Python 3.14+)
- `CLAUDE_CODE_PATH` - Path to Claude executable (default: auto-detect via 'which claude')
**Settings File Format:**
```json
{
"CLAUDE_MEM_MODEL": "claude-haiku-4-5",
"CLAUDE_MEM_WORKER_PORT": "37777"
}
```
## File Locations
@@ -58,15 +75,28 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
- **Chroma**: `~/.claude-mem/chroma/`
- **Usage Logs**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl`
## Requirements
- **Bun** >= 1.0 (all platforms - auto-installed if missing)
- **uv** (all platforms - auto-installed if missing, provides Python for Chroma)
- Node.js >= 18 (build tools only)
## Quick Reference
```bash
npm run build # Compile TypeScript
npm run sync-marketplace # Copy to ~/.claude/plugins
npm run worker:restart # Restart PM2 worker
npm run worker:restart # Restart worker service
npm run worker:status # Check worker status
npm run worker:logs # View worker logs
pm2 list # Check worker status
pm2 delete claude-mem-worker # Force clean start
```
**Viewer UI**: http://localhost:37777
**Worker Logs**: `~/.claude-mem/logs/worker-YYYY-MM-DD.log`
## Documentation
**Public Docs**: https://docs.claude-mem.ai (Mintlify)
**Source**: `docs/public/` - MDX files, edit `docs.json` for navigation
**Deploy**: Auto-deploys from GitHub on push to main
**Local Dev**: `cd docs/public && npx mintlify dev`
+50 -9
View File
@@ -27,6 +27,16 @@
</a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/15496" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/trendshift-badge-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/trendshift-badge.svg">
<img src="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/trendshift-badge.svg" alt="thedotmack/claude-mem | Trendshift" width="250" height="55"/>
</picture>
</a>
</p>
<br>
<p align="center">
@@ -71,6 +81,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
- 🔍 **Skill-Based Search** - Query your project history with mem-search skill (~2,250 token savings)
- 🖥️ **Web Viewer UI** - Real-time memory stream at http://localhost:37777
- 💻 **Claude Desktop Skill** - Search memory from Claude Desktop conversations
- 🔒 **Privacy Control** - Use `<private>` tags to exclude sensitive content from storage
- ⚙️ **Context Configuration** - Fine-grained control over what context gets injected
- 🤖 **Automatic Operation** - No manual intervention required
@@ -108,7 +119,7 @@ npx mintlify dev
- **[Architecture Evolution](https://docs.claude-mem.ai/architecture-evolution)** - The journey from v3 to v5
- **[Hooks Architecture](https://docs.claude-mem.ai/hooks-architecture)** - How Claude-Mem uses lifecycle hooks
- **[Hooks Reference](https://docs.claude-mem.ai/architecture/hooks)** - 7 hook scripts explained
- **[Worker Service](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API & PM2 management
- **[Worker Service](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API & Bun management
- **[Database](https://docs.claude-mem.ai/architecture/database)** - SQLite schema & FTS5 search
- **[Search Architecture](https://docs.claude-mem.ai/architecture/search-architecture)** - Hybrid search with Chroma vector database
@@ -148,7 +159,7 @@ npx mintlify dev
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd (6 hook scripts)
2. **Smart Install** - Cached dependency checker (pre-hook script, not a lifecycle hook)
3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by PM2
3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by Bun
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
5. **mem-search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP)
6. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval
@@ -262,7 +273,8 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
- **Node.js**: 18.0.0 or higher
- **Claude Code**: Latest version with plugin support
- **PM2**: Process manager (bundled - no global install required)
- **Bun**: JavaScript runtime and process manager (auto-installed if missing)
- **uv**: Python package manager for vector search (auto-installed if missing)
- **SQLite 3**: For persistent storage (bundled)
---
@@ -306,18 +318,43 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
## Configuration
**Model Selection:**
Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created with defaults on first run.
**Available Settings:**
| Setting | Default | Description |
|---------|---------|-------------|
| `CLAUDE_MEM_MODEL` | `claude-haiku-4-5` | AI model for observations |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_WORKER_HOST` | `127.0.0.1` | Worker bind address (use `0.0.0.0` for remote access) |
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem` | Data directory location |
| `CLAUDE_MEM_LOG_LEVEL` | `INFO` | Log verbosity (DEBUG, INFO, WARN, ERROR, SILENT) |
| `CLAUDE_MEM_PYTHON_VERSION` | `3.13` | Python version for chroma-mcp |
| `CLAUDE_CODE_PATH` | _(auto-detect)_ | Path to Claude executable |
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject at SessionStart |
**Settings Management:**
```bash
# Edit settings via CLI helper
./claude-mem-settings.sh
# Or edit directly
nano ~/.claude-mem/settings.json
# View current settings
curl http://localhost:37777/api/settings
```
**Environment Variables:**
**Settings File Format:**
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-haiku-4-5)
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
- `CLAUDE_MEM_PYTHON_VERSION` - Python version for uvx/chroma-mcp (default: 3.13)
```json
{
"CLAUDE_MEM_MODEL": "claude-haiku-4-5",
"CLAUDE_MEM_WORKER_PORT": "37777",
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "50"
}
```
See [Configuration Guide](https://docs.claude-mem.ai/configuration) for details.
@@ -361,6 +398,10 @@ If you're experiencing issues, describe the problem to Claude and the troublesho
See [Troubleshooting Guide](https://docs.claude-mem.ai/troubleshooting) for complete solutions.
### Windows Known Issues
**Console Window Visibility**: On Windows, a console window may briefly appear when the worker service starts. This is a cosmetic issue that we're working to resolve. We've prioritized stability by removing a workaround that was causing libuv crashes. The window does not affect functionality and will be addressed in a future release when the MCP SDK provides proper window hiding support.
---
## Contributing
+44
View File
@@ -0,0 +1,44 @@
---
name: mem-search
description: Search your persistent memory database from previous coding sessions. Use when asked about past work, decisions, bugs fixed, or development history.
---
## Overview
Search your local memory database for past sessions, decisions, code changes, and development history. This skill uses the `mem-search` MCP server tools.
## Available MCP tools
Use these tools from the `mem-search` MCP server:
| Tool | Description |
|------|-------------|
| `search` | Unified search across all memory types |
| `decisions` | Find architectural/design decisions |
| `changes` | Find code changes and refactorings |
| `timeline` | Get observations around a specific point in time |
| `find_by_file` | Find observations for specific files |
| `find_by_type` | Filter by type (decision, bugfix, feature, refactor, discovery, change) |
| `find_by_concept` | Find by concept tags |
| `how_it_works` | Understand system architecture and design patterns |
## Common parameters
- `query` - Natural language search query
- `limit` - Max results (1-100, default 20)
- `format` - `index` for titles only (recommended), `full` for complete content
- `type` - Filter: observations, sessions, or prompts
- `obs_type` - Filter observation type: decision, bugfix, feature, refactor, discovery, change
## When to use
- "Did we already solve this?"
- "How did we do X last time?"
- "Find the bug fix for..."
- "What decisions did we make about..."
- "Show me changes to [file]"
- "What work did we do on [project]?"
## Setup requirement
The `mem-search` MCP server must be configured in Claude Desktop settings. See MCP configuration docs.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
# Branch Switching Test Plan: feature/bun-executable
## Overview
This document validates that switching to the `feature/bun-executable` branch will be seamless for users.
## Branch Switching Mechanism
When a user switches branches via the Settings UI:
1. **Branch Switch Request**: User selects `feature/bun-executable` from Settings UI
2. **Validation**: SettingsRoutes validates branch name against allowed list
3. **Git Operations**: BranchManager performs:
- Discard local changes (`git checkout -- .` and `git clean -fd`)
- Fetch from origin (`git fetch origin`)
- Checkout target branch (`git checkout feature/bun-executable`)
- Pull latest (`git pull origin feature/bun-executable`)
4. **Install Dependencies**:
- Clear install marker (`.install-version`)
- Run `npm install` (2 minute timeout)
5. **Worker Restart**: Worker process exits and PM2/supervisor restarts it
## Feature Branch Changes
The `feature/bun-executable` branch makes these key changes:
### Dependencies Removed
- `better-sqlite3` → Uses Bun's built-in SQLite
- `pm2` → Custom worker CLI with process management
- `@types/better-sqlite3`
### New Features
- Auto-installation of Bun runtime in smart-install.js
- Simplified worker management via worker-cli.js
- No native module compilation required (better-sqlite3 removed)
## Installation Validation
### Current Branch → feature/bun-executable
**Step 1: Branch Switch (BranchManager)**
```bash
git checkout feature/bun-executable
git pull origin feature/bun-executable
rm .install-version
npm install # ✅ Works - package.json is npm-compatible
```
**Step 2: First Hook Execution**
```bash
node plugin/scripts/context-hook.js
Calls smart-install.js
Checks if Bun installed → Auto-installs if missing
Runs: bun install (if needed)
```
**Step 3: Worker Management**
- Old: PM2 manages worker-service.cjs
- New: worker-cli.js manages worker as background process
- Transition: Automatic on first worker start command
## Seamless Installation Checklist
- [x] **Branch Validation**: `feature/bun-executable` added to allowedBranches list
- [x] **npm install Compatible**: Feature branch package.json works with npm
- [x] **No Breaking Changes**: No hooks that would fail on first run
- [x] **Auto-Install**: smart-install.js automatically installs Bun if missing
- [x] **Graceful Degradation**: Scripts fall back to node if Bun unavailable
- [x] **No Manual Steps**: User just clicks "Switch Branch" in UI
## Potential Issues & Mitigations
### Issue 1: Bun Not in PATH After Install
**Mitigation**: smart-install.js checks common Bun installation paths and provides clear instructions to user
### Issue 2: PM2 vs Worker CLI Transition
**Mitigation**: Old PM2 worker continues running, new worker CLI starts separately. User can manually stop old PM2 worker if needed.
### Issue 3: Windows Compatibility
**Mitigation**: Feature branch uses PowerShell installer for Windows, curl for Unix/macOS
## Test Results
### Unit Tests
```bash
✓ tests/branch-selector.test.ts (5 tests)
✓ should allow main branch
✓ should allow beta/7.0 branch
✓ should allow feature/bun-executable branch
✓ should reject invalid branch names
✓ should have exactly 3 allowed branches
```
### Integration Tests
```bash
✓ All existing tests pass (42 tests)
✓ No regressions introduced
✓ TypeScript compilation successful
```
## Conclusion
✅ **SEAMLESS INSTALLATION VALIDATED**
The installation process is seamless because:
1. Branch switching uses standard git operations
2. `npm install` works on feature branch
3. Bun auto-installs on first hook execution
4. No manual intervention required
5. Clear error messages if issues occur
6. Backward compatible with existing installations
+561
View File
@@ -0,0 +1,561 @@
# Claude-Mem Smart Install & Plugin Hooks - Comprehensive Analysis
**Generated:** 2025-12-09
**Scope:** Smart install system, all plugin hooks, cross-platform compatibility, error handling, edge cases
---
## Executive Summary
This report provides a comprehensive analysis of claude-mem's smart install system and plugin hook infrastructure. The analysis focuses on cross-platform compatibility, error handling patterns, artificial blockers, and edge case handling.
**Key Findings:**
- ✅ Overall architecture is well-designed with clear separation of concerns
- ⚠️ Multiple cross-platform compatibility issues identified
- ⚠️ Several silent failure patterns that hinder debugging
- ⚠️ Artificial blockers that could prevent legitimate use cases
- ⚠️ Inconsistent timeout values across different components
- ✅ No nested try-catch anti-patterns found
---
## Architecture Overview
### Smart Install System Flow
```
User Invokes Hook
ensureWorkerRunning() [worker-utils.ts]
isWorkerHealthy() → fetch /health endpoint
├─ [HEALTHY] → Continue
└─ [UNHEALTHY] → startWorker()
├─ [Windows] → PowerShell Start-Process (hidden window)
└─ [Unix] → Bun start ecosystem.config.cjs
Wait for health check (15 retries × 1000ms)
├─ [SUCCESS] → Continue
└─ [FAILURE] → Throw error with manual recovery instructions
```
### Plugin Hook Lifecycle
1. **SessionStart** (context-hook.ts + user-message-hook.ts)
- context-hook: Fetches context via HTTP/curl
- user-message-hook: Displays context to user via stderr
2. **UserPromptSubmit** (new-hook.ts)
- Creates/retrieves SDK session
- Strips privacy tags from prompt
- Initializes session via HTTP
3. **PostToolUse** (save-hook.ts)
- Filters skipped tools
- Sends observation to worker via HTTP
4. **Stop** (summary-hook.ts)
- Parses transcript JSONL
- Extracts last user/assistant messages
- Requests summary generation via HTTP
5. **SessionEnd** (cleanup-hook.ts)
- Marks session complete
- Fire-and-forget HTTP request
---
## Cross-Platform Compatibility Issues
### 🔴 CRITICAL: curl Dependency (context-hook.ts)
**Location:** `src/hooks/context-hook.ts:32`
```typescript
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
```
**Issues:**
1. **Windows Compatibility:** curl is not guaranteed to be available on Windows systems (though included in Windows 10 1803+, it may be missing on older systems or custom installations)
2. **Error Handling:** No try-catch around execSync - will throw unhandled exception if curl fails
3. **Redundancy:** Uses curl when JavaScript's native `fetch` is already used everywhere else in the codebase
**Impact:** High - SessionStart hook will crash if curl is unavailable or returns non-zero exit code
**Edge Cases:**
- Corporate proxies blocking curl
- Systems without curl in PATH
- curl returning non-zero exit with valid output (warnings, etc.)
**Recommendation:**
```typescript
// Replace curl with fetch (already used in user-message-hook.ts)
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
const result = await response.text();
```
---
### 🟡 MEDIUM: Platform-Specific Process Spawning (worker-utils.ts)
**Location:** `src/shared/worker-utils.ts:55-93`
**Windows Implementation:**
```typescript
spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process -FilePath 'node' -ArgumentList '${workerScript}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden`
])
```
**Issues:**
1. **PowerShell Dependency:** Assumes PowerShell is available and in PATH
2. **Command Injection Risk:** Worker script path inserted directly into command string without escaping
3. **Process Monitoring:** Windows approach launches detached process with no Bun monitoring - harder to debug/restart
4. **Health Check Timeout:** Comment says "Windows needs longer timeouts" but timeout is same for all platforms (500ms)
**Edge Cases:**
- Windows systems with PowerShell execution policy restrictions
- Paths containing single quotes or special characters
- Windows subsystem for Linux (WSL) environments
- Wine/Proton compatibility layers
**Unix Implementation:**
```typescript
const localBunBase = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'bun');
const bunCommand = existsSync(localBunBase) ? localBunBase : 'bun';
```
**Issues:**
1. **Bun Dependency:** Falls back to global bun if local not found, but doesn't verify it exists
2. **Silent Failure:** If Bun not installed globally, spawnSync will fail with cryptic ENOENT error
**Recommendation:**
- Add bun existence check before spawn
- Implement consistent process monitoring across platforms
- Add path escaping for Windows command construction
- Actually implement longer timeout for Windows if needed
---
### 🟡 MEDIUM: Git Dependency (paths.ts)
**Location:** `src/shared/paths.ts:89-97`
```typescript
export function getCurrentProjectName(): string {
try {
const gitRoot = execSync('git rev-parse --show-toplevel', {
cwd: process.cwd(),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return basename(gitRoot);
} catch {
return basename(process.cwd());
}
}
```
**Issues:**
1. **Git Assumption:** Assumes git is installed and available in PATH
2. **Non-Git Projects:** Silently falls back to cwd basename, but this behavior is undocumented
**Edge Cases:**
- Projects not using git
- Monorepos where cwd !== git root is desired
- Systems without git installed
**Status:** ✅ Already handled with fallback, but could benefit from debug logging
---
## Error Handling Analysis
### 🔴 CRITICAL: Silent Failures Without Logging
#### 1. Settings File Loading (early-settings.ts:20-28)
```typescript
try {
if (existsSync(SETTINGS_PATH)) {
const data = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
const fileValue = data.env?.[key];
if (fileValue !== undefined) return fileValue;
}
} catch {
// Fail silently - fall through to env var
}
```
**Problem:**
- Invalid JSON in settings file fails silently
- File read permission errors fail silently
- Users have no way to know their settings file is being ignored
**Impact:** High - Users may think settings are applied when they're actually using defaults
**Recommendation:**
```typescript
} catch (error) {
logger.warn('SETTINGS', 'Failed to load settings file', { path: SETTINGS_PATH }, error);
}
```
---
#### 2. Worker Startup Failure (worker-utils.ts:104-107)
```typescript
try {
// ... worker startup logic ...
} catch (error) {
// Failed to start worker
return false;
}
```
**Problem:**
- Catches ALL errors during worker startup
- Returns boolean with no information about what failed
- User only gets generic error after all retries exhausted
**Impact:** High - Makes debugging worker startup issues extremely difficult
**Recommendation:**
```typescript
} catch (error) {
logger.error('WORKER', 'Failed to start worker', {}, error as Error);
return false;
}
```
---
#### 3. Worker Health Check (worker-utils.ts:30-40)
```typescript
async function isWorkerHealthy(): Promise<boolean> {
try {
const port = getWorkerPort();
const response = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
return response.ok;
} catch {
return false;
}
}
```
**Problem:**
- Network errors, timeouts, and non-200 responses all indistinguishable
- No logging at all - completely silent
**Impact:** Medium - Hard to debug why health checks fail
**Recommendation:**
```typescript
} catch (error) {
logger.debug('WORKER', 'Health check failed', { port }, error);
return false;
}
```
---
#### 4. Tool Formatting (logger.ts:122-124)
```typescript
try {
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
// ...
} catch {
return toolName;
}
```
**Problem:**
- Invalid JSON in tool input fails silently
- Could mask data corruption issues
**Impact:** Low - Only affects log formatting
**Status:** ✅ Acceptable for log formatting, but could log at DEBUG level
---
### 🟢 GOOD: No Nested Try-Catch Anti-Patterns
Analysis confirmed zero instances of nested try-catch blocks. Error handling is consistently at single level per function.
---
## Artificial Blockers & Unnecessary Checks
### 🔴 CRITICAL: First-Run Detection (user-message-hook.ts:14-40)
```typescript
const nodeModulesPath = join(pluginDir, 'node_modules');
if (!existsSync(nodeModulesPath)) {
// Show first-time setup message
console.error(`...`);
process.exit(3);
}
```
**Problems:**
1. **False Positive:** Will trigger if user manually deletes node_modules (e.g., for troubleshooting)
2. **Installation Race:** Could fail if installation is still in progress
3. **Hook-Level Check:** Runs on EVERY SessionStart, not just actual first run
**Impact:** High - Prevents usage until node_modules exists, even if dependencies are installed elsewhere
**Edge Cases:**
- User runs `rm -rf node_modules` for troubleshooting
- Package manager installation interrupted
- Symlinked node_modules (some package managers)
**Recommendation:**
- Use a `.first-run-complete` marker file instead
- Move check to npm postinstall script
- Make check more robust (check for specific required modules)
---
### 🟡 MEDIUM: Overly Specific Validation (paths.ts:117-119)
```typescript
if (!existsSync(join(commandsDir, 'save.md'))) {
throw new Error('Package commands directory missing required files');
}
```
**Problem:**
- Checks for ONE specific file to validate entire directory
- Hardcoded filename could break if files reorganized
- Error message doesn't specify what's missing
**Impact:** Medium - Could prevent package from working after internal refactoring
**Recommendation:**
- Remove check entirely (let actual command invocation fail with better error)
- Or check all required files if validation is critical
---
### 🟡 MEDIUM: Duplicate Health Endpoints
**Locations:**
- `src/services/worker-service.ts:107` - `/api/health`
- `src/services/worker/http/routes/ViewerRoutes.ts:27` - `/health`
**Usage:**
- `worker-utils.ts` uses `/health`
- `mcp-server.ts` uses `/api/health`
**Problem:**
- Redundant endpoints doing the same thing
- Inconsistent usage across codebase
- Maintenance burden
**Impact:** Low - Both work, but creates confusion
**Recommendation:**
- Standardize on `/api/health` (follows REST convention)
- Remove `/health` endpoint
- Update worker-utils.ts to use `/api/health`
---
## Timeout Configuration Issues
### Inconsistent Timeouts Across Components
| Component | Timeout | Location | Purpose |
|-----------|---------|----------|---------|
| Health check | 500ms | worker-utils.ts:13 | Check if worker alive |
| Worker startup wait | 1000ms | worker-utils.ts:14 | Wait between health checks |
| Worker startup retries | 15x | worker-utils.ts:15 | Max retries (15s total) |
| Hook HTTP requests | 2000ms | cleanup-hook.ts:61, save-hook.ts:70, summary-hook.ts:164 | Send data to worker |
| New hook session init | 5000ms | new-hook.ts:129 | Initialize session |
| Context hook fetch | 5000ms | context-hook.ts:32 | Fetch context via curl |
| User message hook | 5000ms | user-message-hook.ts:52 | Fetch context display |
**Problems:**
1. **Health Check Too Aggressive:** 500ms may be too short for loaded systems or slow network
2. **No Platform Adjustment:** Comment says "Windows needs longer timeouts" but values are same
3. **Hook Timeout Variation:** Some hooks use 2s, others use 5s with no clear reasoning
**Recommendations:**
- Increase health check timeout to 1000ms minimum
- Actually implement longer timeouts for Windows
- Standardize hook timeouts to 5000ms across the board
- Make timeouts configurable via settings
---
## Edge Case Analysis
### Handled Well ✅
1. **JSONL Parsing:** summary-hook.ts continues on malformed lines (60-64, 117-121)
2. **Git Not Available:** paths.ts falls back to cwd basename (89-97)
3. **Settings File Missing:** early-settings.ts falls back to env vars and defaults (20-28)
4. **Privacy Tags:** new-hook.ts handles fully-private prompts (99-109)
5. **Tool Skipping:** save-hook.ts filters low-value tools (24-30)
### Missing Edge Case Handling ⚠️
1. **curl Failure:** context-hook.ts has no error handling for curl failures
2. **Bun Not Installed:** worker-utils.ts assumes bun exists globally
3. **PowerShell Restrictions:** worker-utils.ts doesn't check execution policy
4. **Concurrent Worker Starts:** No locking to prevent multiple hooks from starting worker simultaneously
5. **Port Already In Use:** No detection or recovery if worker port is taken
6. **Zombie Processes:** Windows approach doesn't track PIDs, can't detect/kill zombies
---
## Recommendations Summary
### High Priority 🔴
1. **Replace curl with fetch** in context-hook.ts
- Eliminates external dependency
- Consistent with rest of codebase
- Better error handling
2. **Add logging to silent failures**
- early-settings.ts: Log when settings file fails to load
- worker-utils.ts: Log startup failures with details
- worker-utils.ts: Log health check failures at debug level
3. **Fix first-run detection**
- Use marker file instead of node_modules check
- More reliable and intentional
### Medium Priority 🟡
4. **Verify Bun availability** before attempting to use it
- Check existence before spawn
- Provide clear error message if missing
5. **Implement platform-specific timeouts**
- Actually use longer timeouts on Windows as comment suggests
- Make timeouts configurable
6. **Standardize health endpoints**
- Remove duplicate `/health` endpoint
- Use `/api/health` everywhere
7. **Add path escaping** for Windows PowerShell commands
- Prevent injection issues
- Handle paths with special characters
### Low Priority 🟢
8. **Standardize HTTP timeouts** across all hooks
9. **Add concurrent startup protection** (locking mechanism)
10. **Improve error messages** with actionable recovery steps
---
## Testing Recommendations
### Cross-Platform Testing Needed
1. **Windows Environments:**
- Windows 10 (various versions)
- Windows 11
- Windows Server
- WSL/WSL2
- PowerShell execution policies (Restricted, RemoteSigned, Unrestricted)
2. **Unix Environments:**
- macOS (Intel + Apple Silicon)
- Linux (Ubuntu, Fedora, Arch)
- FreeBSD
3. **Edge Environments:**
- Docker containers
- CI/CD environments
- Systems without git installed
- Systems without curl (or with restricted curl)
- Corporate networks with proxies
- Low-spec systems (slow startup)
### Test Scenarios
1. **Cold Start:** First run with no existing data
2. **Corrupt Settings:** Invalid JSON in settings.json
3. **Missing Dependencies:** No Bun, no git, no curl
4. **Port Conflicts:** Worker port already in use
5. **Rapid Hook Invocations:** Multiple hooks trying to start worker simultaneously
6. **Permission Issues:** Read-only filesystem, restricted execution
7. **Network Issues:** Localhost blocked, slow network
---
## Code Quality Assessment
### Strengths ✅
- Clean separation of concerns (hooks → worker → database)
- No nested try-catch anti-patterns
- Consistent use of modern async/await
- Good use of TypeScript for type safety
- Idempotent database operations
- Clear documentation in critical sections
### Weaknesses ⚠️
- Silent failures hinder debugging
- Inconsistent error handling patterns
- Platform-specific code not fully tested/documented
- Timeout configuration hardcoded and inconsistent
- Some artificial blockers prevent legitimate use cases
### Technical Debt
- Duplicate health endpoints
- curl dependency when fetch available
- Bun dependency on Unix but not Windows (inconsistent monitoring)
- First-run detection using node_modules existence
- Hardcoded timeout values
---
## Conclusion
The claude-mem smart install and plugin hook system is architecturally sound with a well-designed separation of concerns. However, several cross-platform compatibility issues and silent failure patterns could cause problems in production, particularly on Windows systems or in edge case scenarios.
The highest priority improvements are:
1. Removing the curl dependency
2. Adding proper logging to silent failures
3. Fixing the fragile first-run detection
4. Verifying external dependencies before use
These changes would significantly improve debuggability and cross-platform reliability without requiring major architectural changes.
---
**Analysis Methodology:**
- Systematic review of all TypeScript source files
- Static analysis of error handling patterns
- Cross-platform compatibility assessment
- Edge case identification through code path analysis
- Comparison against best practices and KISS principles
**Files Analyzed:**
- src/hooks/*.ts (6 files)
- src/services/worker-service.ts
- src/services/worker/*.ts (10+ files)
- src/servers/mcp-server.ts
- src/shared/*.ts (worker-utils, early-settings, paths)
- src/utils/*.ts (logger, silent-debug, tag-stripping)
+5 -5
View File
@@ -68,9 +68,9 @@ CLAUDE_MEM_PYTHON_VERSION=3.13 # Python version for chroma-mcp
```bash
npm run build # Compile TypeScript (hooks + worker)
npm run sync-marketplace # Copy to ~/.claude/plugins
npm run worker:restart # Restart PM2 worker
npm run worker:restart # Restart Bun worker
npm run worker:logs # View worker logs
pm2 list # Check worker status
bun list # Check worker status
```
---
@@ -918,8 +918,8 @@ esbuild.build({
npm run watch
# Terminal 2: Check worker status
pm2 list
pm2 logs claude-mem-worker
bun list
bun logs claude-mem-worker
# Terminal 3: Test API manually
curl http://localhost:37777/api/health
@@ -1020,7 +1020,7 @@ describe('Worker Integration', () => {
### Manual Testing Checklist
**Phase 1: Connection & Health**
- [ ] Worker starts successfully (`pm2 list`)
- [ ] Worker starts successfully (`bun list`)
- [ ] Health endpoint responds (`curl http://localhost:37777/api/health`)
- [ ] SSE stream connects (`curl http://localhost:37777/stream`)
+83
View File
@@ -0,0 +1,83 @@
# Claude-Mem Hooks Cleanup Todo
## ✅ Phase 1: Delete Dead Code (Modified)
**hook-response.ts**
- [ ] Remove `| string` from HookType union to restore type safety
- [ ] Delete PreCompact branch (lines 23-36, 14 lines)
- [x] ~~Delete pointless branches~~ — SKIP (intentional)
- [x] ~~Simplify wrapper function~~ — SKIP (intentional)
**new-hook.ts**
- [ ] Delete 34-line architecture comment block (lines 1-34)
- [ ] Replace 18 lines of debug logging with single 4-line log call (lines 64-81)
**cleanup-hook.ts**
- [ ] Remove `cwd`, `transcript_path`, `hook_event_name` from SessionEndInput interface
- [ ] Replace 12-line manual mode help with simple error throw
**user-message-hook.ts**
- [ ] Delete all 40 lines of expired announcement code (lines 31-70)
- [ ] Add comment explaining exit code 3: `// exit code 3 = show user message that Claude does NOT receive as context`
---
## ✅ Phase 2: Extract Shared Utilities
- [ ] Create `src/shared/hook-error-handler.ts` with `handleWorkerError()`
- [ ] Update all 4 hooks to use shared error handler (context-hook, new-hook, save-hook, summary-hook)
- [ ] Create `src/shared/transcript-parser.ts` — merge `extractLastUserMessage` + `extractLastAssistantMessage` into single parameterized function
- [ ] Create `src/shared/hook-constants.ts` for exit codes, timeouts
---
## ❌ Phase 3: SKIPPED
_(Entry points stay as-is, hook-response.ts wrapper stays as-is)_
---
## ✅ Phase 4: Restore Type Safety
**context-hook.ts**
- [ ] Make `session_id`, `cwd`, `transcript_path` required in SessionStartInput
- [ ] Remove `[key: string]: any`
- [ ] Remove unused `source` field
- [ ] Keep using `happy_path_error__with_fallback` for defaults (hooks use exit codes, logging tool is appropriate)
**All 4 hook interfaces**
- [ ] Remove `[key: string]: any` from all interfaces
**save-hook.ts**
- [ ] Keep `happy_path_error__with_fallback` usage (it's appropriate for hook context)
**summary-hook.ts**
- [ ] Add timeout (2s) and error logging to spinner stop request
---
## ✅ Phase 5: Relocate Business Logic (Modified)
- [ ] Move `SKIP_TOOLS` from save-hook.ts to worker service
- [ ] Make `SKIP_TOOLS` configurable via settings.json
- [x] ~~Move announcements to database~~ — SKIP
- [x] ~~Merge context-hook + user-message-hook~~ — SKIP (intentionally separate)
---
## Summary
| Action | Count |
| ----------------- | ----- |
| Lines to delete | ~150 |
| New shared files | 3 |
| Interfaces to fix | 4 |
| Items skipped | 5 |
+15 -17
View File
@@ -1,4 +1,9 @@
# Architecture Evolution: The Journey from v3 to v5
---
title: "Architecture Evolution"
description: "How claude-mem evolved from v3 to v5+"
---
# Architecture Evolution
## The Problem We Solved
@@ -46,22 +51,15 @@ const ThemeProvider = ({ children }) => {
**Why It Matters**: Users working in different lighting conditions can now customize the viewer for comfort.
### v5.1.1: PM2 Windows Fix (November 2025)
### v5.1.1: Worker Startup Fix (November 2025) - Now Deprecated
**The Problem**: PM2 startup failed on Windows with ENOENT error
**Note**: This section describes a historical PM2-based approach that has been replaced with Bun in later versions.
**Root Cause**:
```typescript
// ❌ Failed on Windows - PM2 not in PATH
execSync('pm2 start ecosystem.config.cjs');
```
**The Problem**: Worker startup failed on Windows with ENOENT error when using PM2
**The Fix**:
```typescript
// ✅ Use full path to PM2 binary
const PM2_PATH = join(PLUGIN_ROOT, 'node_modules', '.bin', 'pm2');
execSync(`"${PM2_PATH}" start "${ECOSYSTEM_CONFIG}"`);
```
**Historical Solution**: Used full path to PM2 binary instead of relying on PATH
**Current Approach**: The project now uses Bun for process management, which provides better cross-platform compatibility and eliminates these PATH-related issues.
**Impact**: Cross-platform compatibility restored, Windows users can now use claude-mem without issues.
@@ -158,7 +156,7 @@ if (currentVersion !== installedVersion) {
**Cached Check Logic**:
1. Does `node_modules` exist?
2. Does `.install-version` match `package.json` version?
3. Is `better-sqlite3` present?
3. Is `better-sqlite3` present? (Legacy: now uses bun:sqlite which requires no installation)
**Impact**:
- SessionStart hook: 2-5 seconds → 10ms (99.5% faster)
@@ -203,7 +201,7 @@ async function ensureWorkerHealthy() {
**Key Fixes**:
- Fixed race conditions in observation queue processing
- Improved error handling in SDK worker
- Better cleanup of stale PM2 processes
- Better cleanup of stale worker processes
- Enhanced logging for debugging
### v5.0.0: Hybrid Search Architecture (October 2025)
@@ -520,7 +518,7 @@ const response = query({
**Key change from v3:**
- ✅ Stores raw prompts for search
- ✅ Auto-starts PM2 worker
- ✅ Auto-starts worker service
</Tab>
<Tab title="PostToolUse">
+6 -6
View File
@@ -5,17 +5,17 @@ description: "SQLite schema, FTS5 search, and data storage"
# Database Architecture
Claude-Mem uses SQLite 3 with the better-sqlite3 native module for persistent storage and FTS5 for full-text search.
Claude-Mem uses SQLite 3 with the bun:sqlite native module for persistent storage and FTS5 for full-text search.
## Database Location
- **Current**: `~/.claude-mem/claude-mem.db`
**Path**: `~/.claude-mem/claude-mem.db`
**Note**: Despite the README claiming v4.0.0+ moved the database to `${CLAUDE_PLUGIN_ROOT}/data/`, the actual implementation still uses `~/.claude-mem/`.
The database uses SQLite's WAL (Write-Ahead Logging) mode for concurrent reads/writes.
## Database Implementation
**Primary Implementation**: better-sqlite3 (native SQLite module)
**Primary Implementation**: bun:sqlite (native SQLite module)
- Used by: SessionStore and SessionSearch
- Format: Synchronous API with better performance
- **Note**: Database.ts (using bun:sqlite) is legacy code
@@ -301,8 +301,8 @@ Database schema is managed via migrations in `src/services/sqlite/migrations.ts`
- **Indexes**: All foreign keys and frequently queried columns are indexed
- **FTS5**: Full-text search is significantly faster than LIKE queries
- **Triggers**: Automatic synchronization has minimal overhead
- **Connection Pooling**: better-sqlite3 reuses connections efficiently
- **Synchronous API**: better-sqlite3 uses synchronous API for better performance
- **Connection Pooling**: bun:sqlite reuses connections efficiently
- **Synchronous API**: bun:sqlite uses synchronous API for better performance
## Troubleshooting
+2 -2
View File
@@ -446,7 +446,7 @@ sequenceDiagram
else Tool allowed
SaveHook->>SaveHook: Strip privacy tags from input/response
SaveHook->>SaveHook: Ensure worker running<br/>(PM2 health check)
SaveHook->>SaveHook: Ensure worker running<br/>(health check)
SaveHook->>Worker: POST /api/sessions/observations<br/>{ claudeSessionId, tool_name, tool_input, tool_response, cwd }<br/>(fire-and-forget, 2s timeout)
@@ -906,7 +906,7 @@ For developers implementing this pattern on other platforms:
### Worker Service
- [ ] HTTP server on configurable port (default 37777)
- [ ] PM2 or equivalent process management
- [ ] Bun runtime for process management
- [ ] 3 core services: SessionManager, SDKAgent, DatabaseManager
### Hook Implementation
+7 -7
View File
@@ -22,14 +22,14 @@ Claude-Mem operates as a Claude Code plugin with five core components:
|------------------------|-------------------------------------------|
| **Language** | TypeScript (ES2022, ESNext modules) |
| **Runtime** | Node.js 18+ |
| **Database** | SQLite 3 with better-sqlite3 driver |
| **Database** | SQLite 3 with bun:sqlite driver |
| **Vector Store** | ChromaDB (optional, for semantic search) |
| **HTTP Server** | Express.js 4.18 |
| **Real-time** | Server-Sent Events (SSE) |
| **UI Framework** | React + TypeScript |
| **AI SDK** | @anthropic-ai/claude-agent-sdk |
| **Build Tool** | esbuild (bundles TypeScript) |
| **Process Manager** | PM2 |
| **Process Manager** | Bun |
| **Testing** | Node.js built-in test runner |
## Data Flow
@@ -70,7 +70,7 @@ User Query → mem-search Skill Invoked → HTTP API → SessionSearch Service
┌─────────────────────────────────────────────────────────────────┐
│ 1. Session Starts → Context Hook Fires │
│ Starts PM2 worker if needed, injects context from previous │
│ Starts Bun worker if needed, injects context from previous │
│ sessions (configurable observation count) │
└─────────────────────────────────────────────────────────────────┘
@@ -177,13 +177,13 @@ claude-mem/
├── tests/ # Test suite
├── docs/ # Documentation
└── ecosystem.config.cjs # PM2 configuration
└── ecosystem.config.cjs # Process configuration (deprecated)
```
## Component Details
### 1. Plugin Hooks (6 Hooks)
- **context-hook.js** - SessionStart: Starts PM2 worker, injects context
- **context-hook.js** - SessionStart: Starts Bun worker, injects context
- **user-message-hook.js** - UserMessage: Debugging hook
- **new-hook.js** - UserPromptSubmit: Creates session, saves prompt
- **save-hook.js** - PostToolUse: Captures tool executions
@@ -200,12 +200,12 @@ Express.js HTTP server on port 37777 (configurable) with:
- 8 viewer UI HTTP/SSE endpoints
- Async observation processing via Claude Agent SDK
- Real-time updates via Server-Sent Events
- Auto-managed by PM2 process manager
- Auto-managed by Bun
See [Worker Service](/architecture/worker-service) for HTTP API and endpoints.
### 3. Database Layer
SQLite3 with better-sqlite3 driver featuring:
SQLite3 with bun:sqlite driver featuring:
- FTS5 virtual tables for full-text search
- SessionStore for CRUD operations
- SessionSearch for FTS5 queries
@@ -0,0 +1,551 @@
---
title: "PM2 to Bun Migration"
description: "Complete technical documentation for the process management and database driver migration in v7.1.0"
---
# PM2 to Bun Migration: Complete Technical Documentation
**Version**: 7.1.0
**Date**: December 2025
**Migration Type**: Process Management (PM2 → Bun) + Database Driver (better-sqlite3 → bun:sqlite)
## Executive Summary
Claude-mem version 7.1.0 introduces two major architectural migrations:
1. **Process Management**: PM2 → Custom Bun-based ProcessManager
2. **Database Driver**: better-sqlite3 npm package → bun:sqlite runtime module
Both migrations are **automatic** and **transparent** to end users. The first time a hook fires after updating to 7.1.0+, the system performs a one-time cleanup of legacy PM2 processes and transitions to the new architecture.
### Key Benefits
- **Simplified Dependencies**: Removes PM2 and better-sqlite3 npm packages
- **Improved Cross-Platform Support**: Better Windows compatibility
- **Faster Installation**: No native module compilation required
- **Built-in Runtime**: Leverages Bun's built-in process management and SQLite
- **Reduced Complexity**: Custom ProcessManager is simpler than PM2 integration
### Migration Impact
- **Data Preservation**: User data, settings, and database remain unchanged
- **Automatic Cleanup**: Old PM2 processes automatically terminated (all platforms)
- **No User Action Required**: Migration happens automatically on first hook trigger
- **Backward Compatible**: SQLite database format unchanged (only driver changed)
## Architecture Comparison
### Old System (PM2-based)
<AccordionGroup>
<Accordion title="Process Management (PM2)">
**Component**: PM2 (Process Manager 2)
- **Package**: `pm2` npm dependency
- **Process Name**: `claude-mem-worker`
- **Management**: External PM2 daemon manages lifecycle
- **Discovery**: `pm2 list`, `pm2 describe` commands
- **Auto-restart**: PM2 automatically restarts on crash
- **Logs**: `~/.pm2/logs/claude-mem-worker-*.log`
- **PID File**: `~/.pm2/pids/claude-mem-worker.pid`
**Lifecycle Commands**:
```bash
pm2 start <script> # Start worker
pm2 stop claude-mem-worker # Stop worker
pm2 restart claude-mem-worker # Restart worker
pm2 delete claude-mem-worker # Remove from PM2
pm2 logs claude-mem-worker # View logs
```
**Pain Points**:
- Additional npm dependency required
- PM2 daemon must be running
- Potential conflicts with other PM2 processes
- Windows compatibility issues
- Complex configuration for simple use case
</Accordion>
<Accordion title="Database Driver (better-sqlite3)">
**Component**: better-sqlite3
- **Package**: `better-sqlite3` npm package (native module)
- **Installation**: Requires native compilation (node-gyp)
- **Windows**: Requires Visual Studio build tools + Python
- **Import**: `import Database from 'better-sqlite3'`
**Installation Requirements**:
- Node.js development headers
- C++ compiler (gcc/clang on Mac/Linux, MSVC on Windows)
- Python (for node-gyp)
- Windows: Visual Studio Build Tools
</Accordion>
</AccordionGroup>
### New System (Bun-based)
<AccordionGroup>
<Accordion title="Process Management (Custom ProcessManager)">
**Component**: Custom ProcessManager (`src/services/process/ProcessManager.ts`)
- **Package**: Built-in Bun APIs (no external dependency)
- **Process Spawn**: `Bun.spawn()` with detached mode
- **Management**: Direct process control via PID file
- **Discovery**: PID file + process existence check + HTTP health check
- **Auto-restart**: Hook-triggered restart on failure detection
- **Logs**: `~/.claude-mem/logs/worker-YYYY-MM-DD.log`
- **PID File**: `~/.claude-mem/.worker.pid`
- **Port File**: `~/.claude-mem/.worker.port` (new)
**Lifecycle Commands**:
```bash
npm run worker:start # Start worker
npm run worker:stop # Stop worker
npm run worker:restart # Restart worker
npm run worker:status # Check status
npm run worker:logs # View logs
```
**Core Mechanisms**:
1. **PID File Management**:
- File: `~/.claude-mem/.worker.pid`
- Content: Process ID (e.g., "35557")
- Validation: Process existence via `kill(pid, 0)` signal
2. **Port File Management**:
- File: `~/.claude-mem/.worker.port`
- Content: Two lines (port number, PID)
- Purpose: Track port binding and validate PID match
3. **Health Checking**:
- Layer 1: PID file exists?
- Layer 2: Process alive? (`kill(pid, 0)`)
- Layer 3: HTTP health check (`GET /health`)
- All three must pass for "healthy" status
**Advantages**:
- No external dependencies
- Simpler codebase (direct control)
- Better error handling and validation
- Platform-agnostic (Bun handles platform differences)
</Accordion>
<Accordion title="Database Driver (bun:sqlite)">
**Component**: bun:sqlite
- **Package**: Built into Bun runtime (no npm package)
- **Installation**: None required (comes with Bun ≥1.0)
- **Platform**: Works anywhere Bun works
- **Import**: `import { Database } from 'bun:sqlite'`
- **API**: Similar to better-sqlite3 (synchronous)
**Installation Requirements**:
- Bun ≥1.0 (automatically installed if missing)
- No native compilation required
- No platform-specific build tools needed
**Compatibility**:
- SQLite database format: **Unchanged**
- Database file: `~/.claude-mem/claude-mem.db` (same location)
- Query syntax: **Identical** (both use SQLite SQL)
</Accordion>
</AccordionGroup>
## Migration Mechanics
### One-Time PM2 Cleanup
The migration system uses a marker-based approach to perform PM2 cleanup exactly once.
**Implementation**: `src/shared/worker-utils.ts:73-86`
```typescript
// Clean up legacy PM2 (one-time migration)
const pm2MigratedMarker = join(DATA_DIR, '.pm2-migrated');
if (!existsSync(pm2MigratedMarker)) {
try {
spawnSync('pm2', ['delete', 'claude-mem-worker'], { stdio: 'ignore' });
// Mark migration as complete
writeFileSync(pm2MigratedMarker, new Date().toISOString(), 'utf-8');
logger.debug('SYSTEM', 'PM2 cleanup completed and marked');
} catch {
// PM2 not installed or process doesn't exist - still mark as migrated
writeFileSync(pm2MigratedMarker, new Date().toISOString(), 'utf-8');
}
}
```
### Migration Trigger Points
<Steps>
<Step title="Hook Execution">
SessionStart, UserPromptSubmit, or PostToolUse hooks execute using new 7.1.0 code
</Step>
<Step title="Worker Status Check">
`ensureWorkerRunning()` checks if `~/.claude-mem/.worker.pid` exists (it doesn't for first run after update)
</Step>
<Step title="Start Worker Decision">
Worker not running → Call `startWorker()`
</Step>
<Step title="Migration Check">
Check if `~/.claude-mem/.pm2-migrated` exists
</Step>
<Step title="PM2 Cleanup">
Execute `pm2 delete claude-mem-worker` (errors ignored), create marker file
</Step>
<Step title="New Worker Start">
Spawn new Bun-managed worker process with PID and port files
</Step>
</Steps>
### Marker File
**Location**: `~/.claude-mem/.pm2-migrated`
**Content**: ISO 8601 timestamp
```
2025-12-13T00:18:39.673Z
```
**Purpose**:
- One-time migration flag
- Prevents repeated PM2 cleanup on every start
- Persists across restarts and reboots
**Lifecycle**:
- Created: First hook trigger after update to 7.1.0+ (all platforms)
- Updated: Never
- Deleted: Never (user could manually delete to force re-migration)
## User Experience Timeline
### First Session After Update
<Note>
This is the critical migration moment. The process takes approximately 2-5 seconds.
</Note>
**Step-by-Step Execution**:
1. **Hook fires** (SessionStart most common)
2. **Worker status check**: No PID file → worker not running
3. **Migration check**: No marker file → run PM2 cleanup
4. **PM2 cleanup**: `pm2 delete claude-mem-worker` (old worker terminated)
5. **Marker creation**: `~/.claude-mem/.pm2-migrated` with timestamp
6. **New worker start**: Bun process spawned, PID/port files created
7. **Verification**: Process check + HTTP health check
8. **Hook completes**: Claude Code session starts normally
**User Observable Behavior**:
- Slight delay on first startup (PM2 cleanup + new worker spawn)
- No error messages (cleanup failures silently handled)
- Worker appears running via `npm run worker:status`
- Old PM2 worker no longer in `pm2 list`
### Subsequent Sessions
After migration completes, every hook trigger follows the fast path:
1. PID file exists? **YES**
2. Process alive? **YES**
3. HTTP health check? **SUCCESS**
4. Result: Worker already running, done (~50ms)
No migration logic runs on subsequent sessions.
## Platform-Specific Behavior
### Platform Comparison
| Feature | macOS | Linux | Windows |
|---------|-------|-------|---------|
| PM2 Cleanup | Attempted | Attempted | Attempted |
| Marker File | Created | Created | Created |
| Process Signals | POSIX (native) | POSIX (native) | Bun abstraction |
| Bun Support | Full | Full | Full |
| PID File | Yes | Yes | Yes |
| Port File | Yes | Yes | Yes |
| Health Check | HTTP | HTTP | HTTP |
| Migration Delay | ~2-5s first time | ~2-5s first time | ~2-5s first time |
### Platform Notes
<Tabs>
<Tab title="macOS">
- POSIX signal handling works natively
- Bun fully supported
- No platform-specific workarounds needed
</Tab>
<Tab title="Linux">
- Identical behavior to macOS
- POSIX signal handling
- Works on Ubuntu, Debian, RHEL, CentOS, Arch
- Alpine may require glibc (not musl)
</Tab>
<Tab title="Windows">
- PM2 cleanup now runs (safe due to try/catch)
- Bun abstracts signal handling differences
- Path module handles Windows separators
- File locking handled by SQLite
</Tab>
</Tabs>
## Observable Changes
### Command Changes
| Old (PM2) | New (Bun) | Notes |
|-----------|-----------|-------|
| `pm2 list` | `npm run worker:status` | Shows worker status |
| `pm2 start <script>` | `npm run worker:start` | Start worker |
| `pm2 stop claude-mem-worker` | `npm run worker:stop` | Stop worker |
| `pm2 restart claude-mem-worker` | `npm run worker:restart` | Restart worker |
| `pm2 delete claude-mem-worker` | `npm run worker:stop` | Remove worker |
| `pm2 logs claude-mem-worker` | `npm run worker:logs` | View logs |
| `pm2 describe claude-mem-worker` | `npm run worker:status` | Detailed status |
| `pm2 monit` | No equivalent | PM2-specific monitoring |
### File Location Changes
**Logs**:
```
Old: ~/.pm2/logs/claude-mem-worker-out.log
~/.pm2/logs/claude-mem-worker-error.log
New: ~/.claude-mem/logs/worker-YYYY-MM-DD.log
```
**PID Files**:
```
Old: ~/.pm2/pids/claude-mem-worker.pid
New: ~/.claude-mem/.worker.pid
```
**Process State**:
```
Old: PM2 daemon memory (pm2 save)
New: ~/.claude-mem/.worker.pid
~/.claude-mem/.worker.port
~/.claude-mem/.pm2-migrated (all platforms)
```
**Database** (unchanged):
```
Same: ~/.claude-mem/claude-mem.db
```
### User-Visible Changes
**Before Update**:
```bash
$ pm2 list
┌────┬────────────────────┬─────────┬─────────┬──────────┐
│ id │ name │ status │ restart │ uptime │
├────┼────────────────────┼─────────┼─────────┼──────────┤
│ 0 │ claude-mem-worker │ online │ 0 │ 2d 5h │
└────┴────────────────────┴─────────┴─────────┴──────────┘
```
**After Update**:
```bash
$ pm2 list
# Empty - worker no longer managed by PM2
$ npm run worker:status
Worker is running
PID: 35557
Port: 37777
Uptime: 2h 15m
```
### Orphaned Files
After migration, these PM2 files may remain (safe to delete):
```
~/.pm2/ # Entire PM2 directory
~/.pm2/logs/ # Old logs
~/.pm2/pids/ # Old PID files
~/.pm2/pm2.log # PM2 daemon log
~/.pm2/dump.pm2 # PM2 process dump
```
**Cleanup (optional)**:
```bash
# Remove PM2 entirely (if not used for other processes)
pm2 kill
rm -rf ~/.pm2
# Or just remove claude-mem logs
rm -f ~/.pm2/logs/claude-mem-worker-*.log
rm -f ~/.pm2/pids/claude-mem-worker.pid
```
## File System State
### State Directory Structure
**Before Migration** (PM2 system):
```
~/.claude-mem/
├── claude-mem.db # Database (unchanged)
├── chroma/ # Vector embeddings (unchanged)
├── logs/ # Application logs (unchanged)
└── settings.json # User settings (unchanged)
~/.pm2/
├── logs/
│ ├── claude-mem-worker-out.log
│ └── claude-mem-worker-error.log
├── pids/
│ └── claude-mem-worker.pid
└── pm2.log
```
**After Migration** (Bun system):
```
~/.claude-mem/
├── claude-mem.db # Database (same file)
├── chroma/ # Vector embeddings (unchanged)
├── logs/
│ └── worker-2025-12-13.log # New log format
├── settings.json # User settings (unchanged)
├── .worker.pid # NEW: Process ID
├── .worker.port # NEW: Port + PID
└── .pm2-migrated # NEW: Migration marker (all platforms)
~/.pm2/ # Orphaned (safe to delete)
├── logs/ # Old logs (no longer written)
├── pids/ # Old PID (no longer updated)
└── pm2.log # PM2 daemon log (not used)
```
## Edge Cases and Troubleshooting
### Scenario 1: Migration Fails (PM2 Still Running)
<Warning>
This is rare but can happen if PM2 has watch mode enabled or the process is manually restarted.
</Warning>
**Symptoms**:
- `pm2 list` still shows `claude-mem-worker`
- Port conflict errors in logs
- Worker fails to start
**Resolution**:
```bash
# Manual cleanup
pm2 delete claude-mem-worker
pm2 save # Persist the deletion
# Force re-migration (optional)
rm ~/.claude-mem/.pm2-migrated
# Restart worker
npm run worker:restart
```
### Scenario 2: Stale PID File (Process Dead)
**Symptoms**:
- `npm run worker:status` shows "not running"
- `.worker.pid` file exists
- Process ID doesn't exist
**Automatic Recovery**: Next hook trigger detects dead process and starts a fresh worker.
**Manual Resolution**:
```bash
rm ~/.claude-mem/.worker.pid
rm ~/.claude-mem/.worker.port
npm run worker:start
```
### Scenario 3: Port Already in Use
**Error**: `EADDRINUSE: address already in use`
**Resolution**:
```bash
# Check what's using the port
lsof -i :37777
# Kill the process
kill -9 <PID>
# Restart worker
npm run worker:restart
```
### Common Error Messages
| Error | Cause | Resolution |
|-------|-------|------------|
| `EADDRINUSE` | Port already in use | `lsof -i :37777` then kill conflicting process |
| `No such process` | Stale PID file | Automatic cleanup on next hook trigger |
| `pm2: command not found` | PM2 not installed | None needed (error is caught and ignored) |
| `Invalid port X` | Port validation failed | Update `CLAUDE_MEM_WORKER_PORT` in settings |
## Developer Notes
### Testing the Migration
```bash
# 1. Install old version (with PM2)
git checkout <pre-7.1.0-tag>
npm install && npm run build && npm run sync-marketplace
# 2. Start PM2 worker
pm2 start plugin/scripts/worker-cli.js --name claude-mem-worker
# 3. Update to new version
git checkout main
npm install && npm run build && npm run sync-marketplace
# 4. Trigger hook
node plugin/scripts/session-start-hook.js
# 5. Verify migration
pm2 list # Should NOT show claude-mem-worker
cat ~/.claude-mem/.pm2-migrated # Should exist
npm run worker:status # Should show Bun worker running
```
### Architecture Decisions
**Why Custom ProcessManager Instead of PM2?**
1. **Simplicity**: Direct control, no external daemon
2. **Dependencies**: Remove npm dependency
3. **Cross-platform**: Bun handles platform differences
4. **Bundle Size**: Reduce plugin package size
5. **Control**: Fine-grained error handling and validation
**Why One-Time Marker Instead of Always Running PM2 Delete?**
1. **Performance**: Avoid unnecessary process spawning
2. **Idempotency**: Migration runs exactly once
3. **Debugging**: Timestamp shows when migration occurred
4. **Simplicity**: Clear migration state
**Why Run PM2 Cleanup on All Platforms?**
1. **Quality Migration**: Clean up orphaned processes
2. **Consistency**: Same behavior across all platforms
3. **Safety**: Error handling already in place (try/catch)
4. **No Downside**: If PM2 not installed, error is caught and ignored
## Summary
The migration from PM2 to Bun-based ProcessManager is a **one-time, automatic, transparent** transition that:
1. **Removes external dependencies** (PM2, better-sqlite3)
2. **Simplifies architecture** (direct process control)
3. **Improves cross-platform support** (especially Windows)
4. **Preserves user data** (database, settings, logs unchanged)
5. **Requires no user action** (automatic on first hook trigger)
**Key Migration Moment**: First hook trigger after update to 7.1.0+
**Duration**: ~2-5 seconds (one-time delay)
**Impact**: Seamless transition, user-invisible
**Rollback**: Not needed (migration is forward-only, safe)
For most users, the migration will be completely transparent - they'll see no errors, no data loss, and experience improved reliability and simpler troubleshooting going forward.
@@ -397,7 +397,7 @@ Claude translates to appropriate API call.
### For Developers
**Deprecated**: MCP search server (`src/servers/search-server.ts`)
**Renamed**: MCP server (formerly `search-server.ts`, now `src/servers/mcp-server.ts`)
- Source file kept for reference
- No longer built or registered
- MCP configuration removed from `plugin/.mcp.json`
@@ -415,7 +415,7 @@ Claude translates to appropriate API call.
If searches fail, check worker service:
```bash
pm2 list # Check status
npm run worker:status # Check status
npm run worker:restart # Restart worker
npm run worker:logs # View logs
```
+35 -37
View File
@@ -1,20 +1,21 @@
---
title: "Worker Service"
description: "HTTP API and PM2 process management"
description: "HTTP API and Bun process management"
---
# Worker Service
The worker service is a long-running HTTP API built with Express.js and managed by PM2. It processes observations through the Claude Agent SDK separately from hook execution to prevent timeout issues.
The worker service is a long-running HTTP API built with Express.js and managed natively by Bun. It processes observations through the Claude Agent SDK separately from hook execution to prevent timeout issues.
## Overview
- **Technology**: Express.js HTTP server
- **Process Manager**: PM2
- **Runtime**: Bun (auto-installed if missing)
- **Process Manager**: Native Bun process management via ProcessManager
- **Port**: Fixed port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
- **Location**: `src/services/worker-service.ts`
- **Built Output**: `plugin/scripts/worker-service.cjs`
- **Model**: Configurable via `CLAUDE_MEM_MODEL` environment variable (default: claude-sonnet-4-5)
- **Model**: Configurable via `CLAUDE_MEM_MODEL` environment variable (default: sonnet)
## REST API Endpoints
@@ -322,28 +323,15 @@ DELETE /sessions/:sessionDbId
**Note**: As of v4.1.0, the cleanup hook no longer calls this endpoint. Sessions are marked complete instead of deleted to allow graceful worker shutdown.
## PM2 Management
## Bun Process Management
### Configuration
### Overview
The worker is configured via `ecosystem.config.cjs`:
```javascript
module.exports = {
apps: [{
name: 'claude-mem-worker',
script: './plugin/scripts/worker-service.cjs',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
FORCE_COLOR: '1'
}
}]
};
```
The worker is managed by the native `ProcessManager` class which handles:
- Process spawning with Bun runtime
- PID file tracking at `~/.claude-mem/worker.pid`
- Health checks with automatic retry
- Graceful shutdown with SIGTERM/SIGKILL fallback
### Commands
@@ -366,7 +354,18 @@ npm run worker:status
### Auto-Start Behavior
As of v4.0.0, the worker service auto-starts when the SessionStart hook fires. Manual start is optional.
The worker service auto-starts when the SessionStart hook fires. Manual start is optional.
### Bun Requirement
Bun is required to run the worker service. If Bun is not installed, the smart-install script will automatically install it on first run:
- **Windows**: `powershell -c "irm bun.sh/install.ps1 | iex"`
- **macOS/Linux**: `curl -fsSL https://bun.sh/install | bash`
You can also install manually via:
- `winget install Oven-sh.Bun` (Windows)
- `brew install oven-sh/bun/bun` (macOS)
## Claude Agent SDK Integration
@@ -390,14 +389,13 @@ The worker service routes observations to the Claude Agent SDK for AI-powered pr
Set the AI model used for processing via environment variable:
```bash
export CLAUDE_MEM_MODEL=claude-sonnet-4-5
export CLAUDE_MEM_MODEL=sonnet
```
Available models:
- `claude-haiku-4-5` - Fast, cost-efficient
- `claude-sonnet-4-5` - Balanced (default)
- `claude-opus-4` - Most capable
- `claude-3-7-sonnet` - Alternative version
Available shorthand models (forward to latest version):
- `haiku` - Fast, cost-efficient
- `sonnet` - Balanced (default)
- `opus` - Most capable
## Port Allocation
@@ -411,15 +409,15 @@ If port 37777 is in use, the worker will fail to start. Set a custom port via en
## Data Storage
The worker service stores data in the plugin data directory:
The worker service stores data in the user data directory:
```
${CLAUDE_PLUGIN_ROOT}/data/
├── claude-mem.db # SQLite database
├── worker.port # Current worker port file
~/.claude-mem/
├── claude-mem.db # SQLite database (bun:sqlite)
├── worker.pid # PID file for process tracking
├── settings.json # User settings
└── logs/
── worker-out.log # Worker stdout logs
└── worker-error.log # Worker stderr logs
── worker-YYYY-MM-DD.log # Daily rotating logs
```
## Error Handling
+94 -80
View File
@@ -5,18 +5,27 @@ description: "Environment variables and settings for Claude-Mem"
# Configuration
## Environment Variables
## Settings File
| Variable | Default | Description |
Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created with defaults on first run.
### Core Settings
| Setting | Default | Description |
|-------------------------------|---------------------------------|---------------------------------------|
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory (production default) |
| `CLAUDE_CODE_PATH` | Auto-detected | Path to Claude Code CLI (for Windows) |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_MODEL` | `claude-haiku-4-5` | AI model for processing observations |
| `CLAUDE_MEM_MODEL` | `haiku` | AI model for processing observations |
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
| `NODE_ENV` | `production` | Environment mode |
| `FORCE_COLOR` | `1` | Enable colored logs |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_SKIP_TOOLS` | `ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion` | Comma-separated tools to exclude from observations |
### System Configuration
| Setting | Default | Description |
|-------------------------------|---------------------------------|---------------------------------------|
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem` | Data directory location |
| `CLAUDE_MEM_LOG_LEVEL` | `INFO` | Log verbosity (DEBUG, INFO, WARN, ERROR, SILENT) |
| `CLAUDE_MEM_PYTHON_VERSION` | `3.13` | Python version for chroma-mcp |
| `CLAUDE_CODE_PATH` | _(auto-detect)_ | Path to Claude Code CLI (for Windows) |
## Model Configuration
@@ -24,10 +33,11 @@ Configure which AI model processes your observations.
### Available Models
- `claude-haiku-4-5` - Fast, cost-efficient (default)
- `claude-sonnet-4-5` - Balanced
- `claude-opus-4` - Most capable
- `claude-3-7-sonnet` - Alternative version
Shorthand model names automatically forward to the latest version:
- `haiku` - Fast, cost-efficient (default)
- `sonnet` - Balanced
- `opus` - Most capable
### Using the Interactive Script
@@ -35,15 +45,15 @@ Configure which AI model processes your observations.
./claude-mem-settings.sh
```
This script manages `CLAUDE_MEM_MODEL` in `~/.claude/settings.json`.
This script manages settings in `~/.claude-mem/settings.json`.
### Manual Configuration
Edit `~/.claude/settings.json`:
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_MODEL": "claude-haiku-4-5"
"CLAUDE_MEM_MODEL": "haiku"
}
```
@@ -82,7 +92,7 @@ ${CLAUDE_PLUGIN_ROOT}/
│ ├── summary-hook.js # Summary generation hook
│ ├── cleanup-hook.js # Session cleanup hook
│ ├── worker-service.cjs # Worker service (CJS)
│ └── search-server.cjs # MCP search server (CJS)
│ └── mcp-server.cjs # MCP search server (CJS)
└── ui/
└── viewer.html # Web viewer UI bundle
```
@@ -171,33 +181,9 @@ Claude-Mem supports switching between stable and beta versions via the web viewe
See [Beta Features](beta-features) for details on what's available in beta.
## PM2 Configuration
## Worker Service Management
Worker service is managed by PM2 via `ecosystem.config.cjs`:
```javascript
module.exports = {
apps: [{
name: 'claude-mem-worker',
script: './plugin/scripts/worker-service.cjs',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
FORCE_COLOR: '1'
}
}]
};
```
### PM2 Settings
- **instances**: 1 (single instance)
- **autorestart**: true (auto-restart on crash)
- **watch**: false (no file watching)
- **max_memory_restart**: 1G (restart if memory exceeds 1GB)
Worker service is managed by Bun as a background process. The worker auto-starts on first session and runs continuously in the background.
## Context Injection Configuration
@@ -276,7 +262,7 @@ Token economics help you understand the value of cached observations vs. re-read
| Setting | Default | Description |
|---------|---------|-------------|
| **Model** | claude-haiku-4-5 | AI model for generating observations |
| **Model** | haiku | AI model for generating observations |
| **Worker Port** | 37777 | Port for background worker service |
| **MCP search server** | true | Enable Model Context Protocol search tools |
| **Include last summary** | false | Add previous session's summary to context |
@@ -284,56 +270,91 @@ Token economics help you understand the value of cached observations vs. re-read
### Manual Configuration
Settings are stored in `~/.claude-mem/settings.json`. You can also configure via environment variables in `~/.claude/settings.json`:
Settings are stored in `~/.claude-mem/settings.json`:
```json
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100",
"CLAUDE_MEM_CONTEXT_SESSION_COUNT": "20",
"CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES": "bugfix,decision,discovery",
"CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS": "how-it-works,gotcha",
"CLAUDE_MEM_CONTEXT_FULL_COUNT": "10",
"CLAUDE_MEM_CONTEXT_FULL_FIELD": "narrative",
"CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT": "true",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY": "false",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE": "false"
}
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100",
"CLAUDE_MEM_CONTEXT_SESSION_COUNT": "20",
"CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES": "bugfix,decision,discovery",
"CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS": "how-it-works,gotcha",
"CLAUDE_MEM_CONTEXT_FULL_COUNT": "10",
"CLAUDE_MEM_CONTEXT_FULL_FIELD": "narrative",
"CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT": "true",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY": "false",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE": "false"
}
```
**Note**: The Context Settings Modal is the recommended way to configure these settings, as it provides live preview of changes.
**Note**: The Context Settings Modal (at http://localhost:37777) is the recommended way to configure these settings, as it provides live preview of changes.
## Customization
Settings can be customized in `~/.claude-mem/settings.json`.
### Custom Data Directory
For development or testing, override the data directory:
```bash
export CLAUDE_MEM_DATA_DIR=/custom/path
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_DATA_DIR": "/custom/path"
}
```
### Custom Worker Port
If port 37777 is in use:
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_WORKER_PORT": "38000"
}
```
Then restart the worker:
```bash
export CLAUDE_MEM_WORKER_PORT=38000
npm run worker:restart
```
### Custom Model
Use a different AI model:
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_MODEL": "opus"
}
```
Then restart the worker:
```bash
export CLAUDE_MEM_MODEL=claude-opus-4
export CLAUDE_MEM_MODEL=opus
npm run worker:restart
```
### Custom Skip Tools
Control which tools are excluded from observations. Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_SKIP_TOOLS": "ListMcpResourcesTool,SlashCommand,Skill"
}
```
**Default excluded tools:**
- `ListMcpResourcesTool`
- `SlashCommand`
- `Skill`
- `TodoWrite`
- `AskUserQuestion`
**Common customizations:**
- Include TodoWrite: Remove from skip list to track task planning
- Include AskUserQuestion: Remove to capture decision-making conversations
- Skip additional tools: Add tool names to reduce observation noise
Changes take effect on the next tool execution (no worker restart needed).
## Advanced Configuration
### Hook Timeouts
@@ -357,13 +378,7 @@ Recommended values:
### Worker Memory Limit
Modify PM2 memory limit in `ecosystem.config.cjs`:
```javascript
{
max_memory_restart: '2G' // Increase if needed
}
```
The worker service is managed by Bun and will automatically restart if it encounters issues. Memory usage is typically low (~100-200MB).
### Logging Verbosity
@@ -405,13 +420,12 @@ npm run worker:logs
### Invalid Model Name
If you specify an invalid model name, the worker will fall back to `claude-haiku-4-5` and log a warning.
If you specify an invalid model name, the worker will fall back to `haiku` and log a warning.
Valid models:
- claude-haiku-4-5
- claude-sonnet-4-5
- claude-opus-4
- claude-3-7-sonnet
Valid shorthand models (forward to latest version):
- haiku
- sonnet
- opus
### Port Already in Use
+6 -1
View File
@@ -1,4 +1,9 @@
# Context Engineering for AI Agents: Best Practices Cheat Sheet
---
title: "Context Engineering"
description: "Best practices for curating optimal token sets for AI agents"
---
# Context Engineering for AI Agents
## Core Principle
**Find the smallest possible set of high-signal tokens that maximize the likelihood of your desired outcome.**
+4 -4
View File
@@ -33,7 +33,7 @@ The build process uses esbuild to compile TypeScript:
1. Compiles TypeScript to JavaScript
2. Creates standalone executables for each hook in `plugin/scripts/`
3. Bundles MCP search server to `plugin/scripts/search-server.cjs`
3. Bundles MCP search server to `plugin/scripts/mcp-server.cjs`
4. Bundles worker service to `plugin/scripts/worker-service.cjs`
5. Bundles web viewer UI to `plugin/ui/viewer.html`
@@ -41,7 +41,7 @@ The build process uses esbuild to compile TypeScript:
- Hook executables: `*-hook.js` (ESM format)
- Smart installer: `smart-install.js` (ESM format)
- Worker service: `worker-service.cjs` (CJS format)
- Search server: `search-server.cjs` (CJS format)
- MCP server: `mcp-server.cjs` (CJS format)
- Viewer UI: `viewer.html` (self-contained HTML bundle)
### Build Scripts
@@ -342,7 +342,7 @@ npm test
### Adding MCP Search Tools
1. Add tool definition in `src/servers/search-server.ts`:
1. Add tool definition in `src/servers/mcp-server.ts`:
```typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -650,7 +650,7 @@ rm -rf plugin/scripts/*.js plugin/scripts/*.cjs
1. Kill existing process:
```bash
pm2 delete claude-mem-worker
npm run worker:stop
```
2. Check port:
+4 -1
View File
@@ -37,7 +37,9 @@
"installation",
"usage/getting-started",
"usage/search-tools",
"usage/claude-desktop",
"usage/private-tags",
"usage/export-import",
"beta-features"
]
},
@@ -69,7 +71,8 @@
"architecture/hooks",
"architecture/worker-service",
"architecture/database",
"architecture/search-architecture"
"architecture/search-architecture",
"architecture/pm2-to-bun-migration"
]
}
]
+20 -37
View File
@@ -85,9 +85,8 @@ Claude-Mem uses 6 lifecycle hook scripts across 5 lifecycle events, plus 1 pre-h
2. Only runs `npm install` when necessary:
- First-time installation
- Version changed in package.json
- Critical dependency missing (better-sqlite3)
3. Provides Windows-specific error messages
4. Starts PM2 worker service
4. Starts Bun worker service
**Configuration:**
```json
@@ -215,7 +214,7 @@ Claude-Mem uses 6 lifecycle hook scripts across 5 lifecycle events, plus 1 pre-h
1. Reads user prompt and session ID from stdin
2. Creates new session record in SQLite
3. Saves raw user prompt for full-text search (v4.2.0+)
4. Starts PM2 worker service if not running
4. Starts Bun worker service if not running
5. Returns immediately (non-blocking)
**Configuration:**
@@ -512,49 +511,33 @@ sequenceDiagram
└─────────────────────────────────────────────────────────┘
```
### PM2 Process Management
### Bun Process Management
**Technology:** PM2 (process manager for Node.js)
**Technology:** Bun (JavaScript runtime and process manager)
**Why PM2:**
**Why Bun:**
- Auto-restart on failure
- Log management
- Process monitoring
- Fast startup and low memory footprint
- Built-in TypeScript support
- Cross-platform (works on macOS, Linux, Windows)
- No systemd/launchd needed
**Configuration:**
```javascript
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'claude-mem-worker',
script: './plugin/scripts/worker-service.cjs',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '500M',
env: {
NODE_ENV: 'production',
CLAUDE_MEM_WORKER_PORT: 37777
}
}]
};
```
- No separate process manager needed
**Worker lifecycle:**
```bash
# Started by new-hook (if not running)
pm2 start ecosystem.config.cjs
# Started by hooks automatically (if not running)
npm run worker:start
# Status check
pm2 status claude-mem-worker
npm run worker:status
# View logs
pm2 logs claude-mem-worker
npm run worker:logs
# Restart
pm2 restart claude-mem-worker
npm run worker:restart
# Stop
npm run worker:stop
```
### Worker HTTP API
@@ -632,7 +615,7 @@ try {
**Failure modes:**
- Database locked → Skip observation, log error
- Worker crashed → Auto-restart via PM2
- Worker crashed → Auto-restart via Bun
- Network issue → Retry with exponential backoff
- Disk full → Warn user, disable memory
@@ -708,8 +691,8 @@ claude --debug
**Debugging:**
1. Check database: `sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM observation_queue"`
2. Verify session exists: `SELECT * FROM sdk_sessions`
3. Check worker status: `pm2 status`
4. View worker logs: `pm2 logs claude-mem-worker`
3. Check worker status: `npm run worker:status`
4. View worker logs: `npm run worker:logs`
</Accordion>
</AccordionGroup>
@@ -761,7 +744,7 @@ claude --debug
**Why smart-install is sometimes slow:**
- First-time: Full npm install (2-5 seconds)
- Cached: Version check only (~10ms)
- Version change: Full npm install + PM2 restart
- Version change: Full npm install + worker restart
**Optimization (v5.0.3):**
- Version caching with `.install-version` marker
+16 -16
View File
@@ -16,7 +16,7 @@ Install Claude-Mem directly from the plugin marketplace:
That's it! The plugin will automatically:
- Download prebuilt binaries (no compilation needed)
- Install all dependencies (including PM2 and SQLite binaries)
- Install all dependencies (including SQLite binaries)
- Configure hooks for session lifecycle management
- Auto-start the worker service on first session
@@ -26,7 +26,7 @@ Start a new Claude Code session and you'll see context from previous sessions au
- **Node.js**: 18.0.0 or higher
- **Claude Code**: Latest version with plugin support
- **PM2**: Process manager (bundled with plugin - no global install required)
- **Bun**: JavaScript runtime and process manager (auto-installed if missing)
- **SQLite 3**: For persistent storage (bundled)
## Advanced Installation
@@ -69,12 +69,14 @@ cat plugin/hooks/hooks.json
#### 3. Data Directory Location
v4.0.0+ stores data in `${CLAUDE_PLUGIN_ROOT}/data/`:
- Database: `${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db`
- Worker port file: `${CLAUDE_PLUGIN_ROOT}/data/worker.port`
- Logs: `${CLAUDE_PLUGIN_ROOT}/data/logs/`
Data is stored in `~/.claude-mem/`:
- Database: `~/.claude-mem/claude-mem.db`
- PID file: `~/.claude-mem/.worker.pid`
- Port file: `~/.claude-mem/.worker.port`
- Logs: `~/.claude-mem/logs/worker-YYYY-MM-DD.log`
- Settings: `~/.claude-mem/settings.json`
For development/testing, you can override:
Override with environment variable:
```bash
export CLAUDE_MEM_DATA_DIR=/custom/path
```
@@ -91,19 +93,17 @@ npm run worker:logs
npm run test:context
```
## Upgrading from v3.x
## Upgrading
**BREAKING CHANGES - Please Read:**
Upgrades are automatic when updating via the plugin marketplace. Key changes in recent versions:
v4.0.0 introduces breaking changes:
**v7.1.0**: PM2 replaced with native Bun process management. Migration is automatic on first hook trigger.
- **Data Location Changed**: Database moved from `~/.claude-mem/` to `${CLAUDE_PLUGIN_ROOT}/data/` (inside plugin directory)
- **Fresh Start Required**: No automatic migration from v3.x. You must start with a clean database
- **Worker Auto-Starts**: Worker service now starts automatically - no manual `npm run worker:start` needed
- **MCP Search Server**: 7 new search tools with full-text search and citations
- **Enhanced Architecture**: Improved plugin integration and data organization
**v7.0.0+**: 11 configuration settings, dual-tag privacy system.
See [CHANGELOG](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md) for complete details.
**v5.4.0+**: Skill-based search replaces MCP tools, saving ~2,250 tokens per session.
See [CHANGELOG](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md) for complete version history.
## Next Steps
+13 -14
View File
@@ -58,7 +58,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
**Core Components:**
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd (6 hook scripts)
2. **Smart Install** - Cached dependency checker (pre-hook script)
3. **Worker Service** - HTTP API on port 37777 managed by PM2
3. **Worker Service** - HTTP API on port 37777 managed by Bun
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
5. **mem-search Skill** - Query historical context with natural language
6. **Web Viewer UI** - Real-time visualization with SSE and infinite scroll
@@ -69,26 +69,25 @@ See [Architecture Overview](architecture/overview) for details.
- **Node.js**: 18.0.0 or higher
- **Claude Code**: Latest version with plugin support
- **PM2**: Process manager (bundled - no global install required)
- **Bun**: JavaScript runtime and process manager (auto-installed if missing)
- **SQLite 3**: For persistent storage (bundled)
## What's New
**v6.4.9 - Context Configuration Settings:**
- 11 new settings for fine-grained control over context injection
- Configure token economics display, observation filtering by type/concept
**v7.1.0 - Bun Migration:**
- Replaced PM2 with native Bun process management
- Switched from better-sqlite3 to bun:sqlite for faster database access
- Automatic one-time migration on first hook trigger
- Simplified cross-platform support
**v6.4.0 - Dual-Tag Privacy System:**
- `<private>` tags for user-controlled privacy - wrap sensitive content to exclude from storage
- Edge processing ensures private content never reaches database
**v6.3.0 - Version Channel:**
- Switch between stable and beta versions from the web viewer UI
**v7.0.0 - Context Configuration:**
- 11 settings for fine-grained control over context injection
- Dual-tag privacy system (`<private>` tags)
**Previous Highlights:**
- **v6.0.0**: Major session management & transcript processing improvements
- **v5.5.0**: mem-search skill enhancement with 100% effectiveness rate
- **v5.4.0**: Skill-based search architecture (~2,250 tokens saved per session)
- **v5.5.0**: mem-search skill with 100% effectiveness rate
- **v5.4.0**: Skill-based search (~2,250 tokens saved per session)
- **v5.1.0**: Web viewer UI at http://localhost:37777
## Next Steps
+5 -5
View File
@@ -57,9 +57,9 @@ CLAUDE_MEM_PYTHON_VERSION=3.13 # Python version for chroma-mcp
```bash
npm run build # Compile TypeScript (hooks + worker)
npm run sync-marketplace # Copy to ~/.claude/plugins
npm run worker:restart # Restart PM2 worker
npm run worker:restart # Restart worker
npm run worker:logs # View worker logs
pm2 list # Check worker status
npm run worker:status # Check worker status
```
## Worker Architecture
@@ -1132,8 +1132,8 @@ esbuild.build({
</Step>
<Step title="Terminal 2: Check worker status">
```bash
pm2 list
pm2 logs claude-mem-worker
npm run worker:status
npm run worker:logs
```
</Step>
<Step title="Terminal 3: Test API manually">
@@ -1238,7 +1238,7 @@ describe('Worker Integration', () => {
<AccordionGroup>
<Accordion title="Phase 1: Connection & Health">
- [ ] Worker starts successfully (`pm2 list`)
- [ ] Worker starts successfully (`npm run worker:status`)
- [ ] Health endpoint responds (`curl http://localhost:37777/api/health`)
- [ ] SSE stream connects (`curl http://localhost:37777/stream`)
</Accordion>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 250 53" width="250" height="55" data-date-format="longDate">
<rect xmlns="http://www.w3.org/2000/svg" stroke="#b5a0d9" stroke-width="1" fill="#1a1a1a" x="0.5" y="0.5" width="249" height="53" rx="10"/>
<foreignObject width="198" height="17" style="font-size: 9px;color: rgb(200, 180, 230);font-family: Arial;font-weight: 400;text-align: center;letter-spacing: 0em;line-height: 1.5;" x="6" y="10" selection="true">
<div xmlns="http://www.w3.org/1999/xhtml">GITHUB TRENDING</div>
</foreignObject>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="&#x421;&#x43B;&#x43E;&#x439;_1" viewBox="0 0 80 80" width="48" height="45" x="10" y="8">
<path fill="#b5a0d9" d="M70.71,40.31C75.74,44.3,80,37.86,80,37.86s-5.64-2.17-8.55,0.61c0.59-1.62,1.02-3.31,1.28-5.01 c4.08,2.16,6.44-2.95,6.44-2.95s-4.41-0.97-6.26,1.4c0.08-0.91,0.12-1.82,0.1-2.73c-0.01-0.36-0.02-0.73-0.05-1.09 c2.96-3.68-1.73-6.99-1.73-6.99s-2.14,5.09,0.98,7.09c0.02,0.33,0.03,0.66,0.03,1c0.01,0.76-0.03,1.52-0.1,2.27 c-0.85-2.69-4.91-3.69-4.91-3.69s-0.13,5.78,4.68,5.48c-0.28,1.69-0.73,3.35-1.34,4.95c-0.19-4.03-5.79-6.33-5.79-6.33 s-1.33,7.55,5.01,8.16c-0.38,0.8-0.8,1.57-1.25,2.32c-0.56,0.95-1.21,1.84-1.89,2.71c0.97-3.99-3.96-7.72-3.96-7.72 s-3.18,6.94,2.73,9.15c-0.38,0.43-0.8,0.81-1.2,1.21c-0.21,0.2-0.43,0.38-0.64,0.58l-0.32,0.29c-0.11,0.09-0.22,0.18-0.33,0.27 l-0.67,0.54l-0.7,0.51c-0.08,0.05-0.16,0.11-0.23,0.16c1.62-3.42-2.07-7.77-2.07-7.77s-4.21,5.55,0.49,8.78 c-1.34,0.79-2.74,1.45-4.2,1.98c1.91-2.59-0.23-6.89-0.23-6.89s-4.66,3.77-1.52,7.46c-1.15,0.33-2.33,0.57-3.51,0.74 c1.46-1.68,0.55-4.83,0.55-4.83s-3.7,2.03-2.18,5c-0.52,0.03-1.05,0.07-1.57,0.06c-0.29,0-0.57,0.01-0.86,0l-0.86-0.04 c-0.85-0.06-1.7-0.15-2.54-0.28l0.68-0.27l0.42-0.17l0.41-0.19l0.82-0.38c0,0,0.01,0,0.01,0c0.39-0.18,0.55-0.65,0.37-1.03 c-0.18-0.39-0.65-0.55-1.03-0.37l-0.04,0.02l-0.77,0.37l-0.39,0.18l-0.39,0.16l-0.79,0.33l-0.8,0.29l-0.4,0.14l-0.41,0.12L40,53.6 l-0.51-0.15l-0.41-0.12l-0.4-0.14l-0.8-0.29l-0.79-0.33l-0.39-0.16l-0.39-0.18l-0.77-0.37l-0.04-0.02c0,0,0,0-0.01,0 c-0.39-0.18-0.85-0.01-1.03,0.38c-0.18,0.39-0.01,0.85,0.38,1.03l0.82,0.38l0.41,0.19l0.42,0.17l0.68,0.27 c-0.84,0.14-1.69,0.22-2.54,0.28l-0.86,0.04c-0.29,0.01-0.57,0-0.86,0c-0.53,0.01-1.05-0.03-1.57-0.06c1.51-2.98-2.18-5-2.18-5 s-0.92,3.15,0.55,4.83c-1.19-0.16-2.36-0.41-3.51-0.74c3.15-3.7-1.52-7.46-1.52-7.46s-2.14,4.31-0.23,6.89 c-1.46-0.53-2.86-1.19-4.2-1.98c4.7-3.22,0.49-8.78,0.49-8.78s-3.69,4.34-2.07,7.77c-0.08-0.05-0.16-0.1-0.23-0.16l-0.7-0.51 l-0.67-0.54c-0.11-0.09-0.23-0.18-0.33-0.27l-0.32-0.29c-0.21-0.19-0.43-0.38-0.64-0.58c-0.4-0.4-0.82-0.79-1.2-1.21 c5.91-2.21,2.73-9.15,2.73-9.15s-4.93,3.73-3.96,7.72c-0.68-0.86-1.33-1.76-1.89-2.71c-0.46-0.75-0.87-1.53-1.25-2.32 c6.33-0.61,5.01-8.16,5.01-8.16s-5.6,2.31-5.79,6.33c-0.61-1.6-1.06-3.26-1.34-4.95c4.81,0.3,4.68-5.48,4.68-5.48 s-4.05,0.99-4.91,3.69c-0.07-0.76-0.1-1.51-0.1-2.27c0-0.33,0.01-0.66,0.03-1c3.11-2.01,0.98-7.09,0.98-7.09s-4.69,3.31-1.73,6.99 C7,28.46,6.99,28.82,6.98,29.18c-0.02,0.91,0.01,1.82,0.1,2.73c-1.84-2.38-6.26-1.4-6.26-1.4s2.37,5.11,6.44,2.95 c0.26,1.71,0.69,3.39,1.28,5.01C5.64,35.69,0,37.86,0,37.86s4.26,6.43,9.29,2.45c0.39,0.87,0.83,1.72,1.31,2.54 c0.47,0.83,1.01,1.63,1.58,2.4C8.71,43.7,4.11,47,4.11,47s5.7,5.1,9.56,0.08c0.04,0.04,0.07,0.08,0.11,0.12 c0.39,0.45,0.82,0.87,1.24,1.3c0.21,0.21,0.44,0.41,0.66,0.61l0.33,0.3c0.11,0.1,0.23,0.19,0.34,0.29l0.69,0.57l0.23,0.17 c-3.34-0.34-6.58,3.29-6.58,3.29s6.19,3.47,8.69-1.83c1.2,0.75,2.47,1.41,3.78,1.96c-2.76,0.6-4.62,4.13-4.62,4.13 s5.89,1.62,6.98-3.26c1.03,0.32,2.07,0.58,3.13,0.78c-1.63,0.99-2.39,3.38-2.39,3.38s4.31,0.39,4.61-3.07 c0.07,0.01,0.14,0.02,0.21,0.02c0.6,0.04,1.2,0.1,1.8,0.09c0.3,0,0.6,0.02,0.9,0.01l0.9-0.03c1.2-0.07,2.41-0.18,3.59-0.42 l0.45-0.08c0.15-0.03,0.29-0.07,0.44-0.1L40,55.13l0.81,0.19c0.15,0.03,0.29,0.07,0.44,0.1l0.45,0.08c1.18,0.23,2.39,0.35,3.59,0.42 l0.9,0.03c0.3,0.01,0.6-0.01,0.9-0.01c0.6,0,1.2-0.06,1.8-0.09c0.07-0.01,0.14-0.02,0.21-0.02c0.31,3.45,4.61,3.07,4.61,3.07 s-0.76-2.39-2.39-3.38c1.06-0.2,2.11-0.46,3.13-0.78c1.09,4.88,6.98,3.26,6.98,3.26s-1.86-3.52-4.62-4.13 c1.31-0.55,2.57-1.21,3.78-1.96c2.5,5.3,8.69,1.83,8.69,1.83s-3.24-3.63-6.58-3.29l0.23-0.17l0.69-0.57 c0.11-0.1,0.23-0.19,0.34-0.29l0.33-0.3c0.22-0.2,0.45-0.4,0.66-0.61c0.42-0.43,0.85-0.84,1.24-1.3c0.03-0.04,0.07-0.08,0.11-0.12 C70.19,52.1,75.89,47,75.89,47s-4.6-3.31-8.08-1.75c0.57-0.77,1.11-1.56,1.58-2.4C69.88,42.03,70.31,41.18,70.71,40.31z"/>
</svg>
<foreignObject width="230" height="35" style="font-size: 14px;color: rgb(200, 180, 230);font-family: Arial;font-weight: 700;text-align: left;letter-spacing: 0em;line-height: 1.5;" x="64" y="24">
<div xmlns="http://www.w3.org/1999/xhtml">#1 Repository Of The Day</div>
</foreignObject>
<foreignObject width="141" height="36" style="font-size: 18px;color: rgb(180, 160, 217);font-family: Arial;font-weight: 400;text-align: center;letter-spacing: 0em;line-height: 1.5;" x="-36" y="9">
<div xmlns="http://www.w3.org/1999/xhtml">1</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 250 53" width="250" height="55" data-date-format="longDate">
<rect xmlns="http://www.w3.org/2000/svg" stroke="#4a0e99" stroke-width="1" fill="#FFFFFF" x="0.5" y="0.5" width="249" height="53" rx="10"/>
<foreignObject width="198" height="17" style="font-size: 9px;color: rgb(67, 39, 135);font-family: Arial;font-weight: 400;text-align: center;letter-spacing: 0em;line-height: 1.5;" x="6" y="10" selection="true">
<div xmlns="http://www.w3.org/1999/xhtml">GITHUB TRENDING</div>
</foreignObject>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="&#x421;&#x43B;&#x43E;&#x439;_1" viewBox="0 0 80 80" width="48" height="45" x="10" y="8">
<path fill="#49278e" d="M70.71,40.31C75.74,44.3,80,37.86,80,37.86s-5.64-2.17-8.55,0.61c0.59-1.62,1.02-3.31,1.28-5.01 c4.08,2.16,6.44-2.95,6.44-2.95s-4.41-0.97-6.26,1.4c0.08-0.91,0.12-1.82,0.1-2.73c-0.01-0.36-0.02-0.73-0.05-1.09 c2.96-3.68-1.73-6.99-1.73-6.99s-2.14,5.09,0.98,7.09c0.02,0.33,0.03,0.66,0.03,1c0.01,0.76-0.03,1.52-0.1,2.27 c-0.85-2.69-4.91-3.69-4.91-3.69s-0.13,5.78,4.68,5.48c-0.28,1.69-0.73,3.35-1.34,4.95c-0.19-4.03-5.79-6.33-5.79-6.33 s-1.33,7.55,5.01,8.16c-0.38,0.8-0.8,1.57-1.25,2.32c-0.56,0.95-1.21,1.84-1.89,2.71c0.97-3.99-3.96-7.72-3.96-7.72 s-3.18,6.94,2.73,9.15c-0.38,0.43-0.8,0.81-1.2,1.21c-0.21,0.2-0.43,0.38-0.64,0.58l-0.32,0.29c-0.11,0.09-0.22,0.18-0.33,0.27 l-0.67,0.54l-0.7,0.51c-0.08,0.05-0.16,0.11-0.23,0.16c1.62-3.42-2.07-7.77-2.07-7.77s-4.21,5.55,0.49,8.78 c-1.34,0.79-2.74,1.45-4.2,1.98c1.91-2.59-0.23-6.89-0.23-6.89s-4.66,3.77-1.52,7.46c-1.15,0.33-2.33,0.57-3.51,0.74 c1.46-1.68,0.55-4.83,0.55-4.83s-3.7,2.03-2.18,5c-0.52,0.03-1.05,0.07-1.57,0.06c-0.29,0-0.57,0.01-0.86,0l-0.86-0.04 c-0.85-0.06-1.7-0.15-2.54-0.28l0.68-0.27l0.42-0.17l0.41-0.19l0.82-0.38c0,0,0.01,0,0.01,0c0.39-0.18,0.55-0.65,0.37-1.03 c-0.18-0.39-0.65-0.55-1.03-0.37l-0.04,0.02l-0.77,0.37l-0.39,0.18l-0.39,0.16l-0.79,0.33l-0.8,0.29l-0.4,0.14l-0.41,0.12L40,53.6 l-0.51-0.15l-0.41-0.12l-0.4-0.14l-0.8-0.29l-0.79-0.33l-0.39-0.16l-0.39-0.18l-0.77-0.37l-0.04-0.02c0,0,0,0-0.01,0 c-0.39-0.18-0.85-0.01-1.03,0.38c-0.18,0.39-0.01,0.85,0.38,1.03l0.82,0.38l0.41,0.19l0.42,0.17l0.68,0.27 c-0.84,0.14-1.69,0.22-2.54,0.28l-0.86,0.04c-0.29,0.01-0.57,0-0.86,0c-0.53,0.01-1.05-0.03-1.57-0.06c1.51-2.98-2.18-5-2.18-5 s-0.92,3.15,0.55,4.83c-1.19-0.16-2.36-0.41-3.51-0.74c3.15-3.7-1.52-7.46-1.52-7.46s-2.14,4.31-0.23,6.89 c-1.46-0.53-2.86-1.19-4.2-1.98c4.7-3.22,0.49-8.78,0.49-8.78s-3.69,4.34-2.07,7.77c-0.08-0.05-0.16-0.1-0.23-0.16l-0.7-0.51 l-0.67-0.54c-0.11-0.09-0.23-0.18-0.33-0.27l-0.32-0.29c-0.21-0.19-0.43-0.38-0.64-0.58c-0.4-0.4-0.82-0.79-1.2-1.21 c5.91-2.21,2.73-9.15,2.73-9.15s-4.93,3.73-3.96,7.72c-0.68-0.86-1.33-1.76-1.89-2.71c-0.46-0.75-0.87-1.53-1.25-2.32 c6.33-0.61,5.01-8.16,5.01-8.16s-5.6,2.31-5.79,6.33c-0.61-1.6-1.06-3.26-1.34-4.95c4.81,0.3,4.68-5.48,4.68-5.48 s-4.05,0.99-4.91,3.69c-0.07-0.76-0.1-1.51-0.1-2.27c0-0.33,0.01-0.66,0.03-1c3.11-2.01,0.98-7.09,0.98-7.09s-4.69,3.31-1.73,6.99 C7,28.46,6.99,28.82,6.98,29.18c-0.02,0.91,0.01,1.82,0.1,2.73c-1.84-2.38-6.26-1.4-6.26-1.4s2.37,5.11,6.44,2.95 c0.26,1.71,0.69,3.39,1.28,5.01C5.64,35.69,0,37.86,0,37.86s4.26,6.43,9.29,2.45c0.39,0.87,0.83,1.72,1.31,2.54 c0.47,0.83,1.01,1.63,1.58,2.4C8.71,43.7,4.11,47,4.11,47s5.7,5.1,9.56,0.08c0.04,0.04,0.07,0.08,0.11,0.12 c0.39,0.45,0.82,0.87,1.24,1.3c0.21,0.21,0.44,0.41,0.66,0.61l0.33,0.3c0.11,0.1,0.23,0.19,0.34,0.29l0.69,0.57l0.23,0.17 c-3.34-0.34-6.58,3.29-6.58,3.29s6.19,3.47,8.69-1.83c1.2,0.75,2.47,1.41,3.78,1.96c-2.76,0.6-4.62,4.13-4.62,4.13 s5.89,1.62,6.98-3.26c1.03,0.32,2.07,0.58,3.13,0.78c-1.63,0.99-2.39,3.38-2.39,3.38s4.31,0.39,4.61-3.07 c0.07,0.01,0.14,0.02,0.21,0.02c0.6,0.04,1.2,0.1,1.8,0.09c0.3,0,0.6,0.02,0.9,0.01l0.9-0.03c1.2-0.07,2.41-0.18,3.59-0.42 l0.45-0.08c0.15-0.03,0.29-0.07,0.44-0.1L40,55.13l0.81,0.19c0.15,0.03,0.29,0.07,0.44,0.1l0.45,0.08c1.18,0.23,2.39,0.35,3.59,0.42 l0.9,0.03c0.3,0.01,0.6-0.01,0.9-0.01c0.6,0,1.2-0.06,1.8-0.09c0.07-0.01,0.14-0.02,0.21-0.02c0.31,3.45,4.61,3.07,4.61,3.07 s-0.76-2.39-2.39-3.38c1.06-0.2,2.11-0.46,3.13-0.78c1.09,4.88,6.98,3.26,6.98,3.26s-1.86-3.52-4.62-4.13 c1.31-0.55,2.57-1.21,3.78-1.96c2.5,5.3,8.69,1.83,8.69,1.83s-3.24-3.63-6.58-3.29l0.23-0.17l0.69-0.57 c0.11-0.1,0.23-0.19,0.34-0.29l0.33-0.3c0.22-0.2,0.45-0.4,0.66-0.61c0.42-0.43,0.85-0.84,1.24-1.3c0.03-0.04,0.07-0.08,0.11-0.12 C70.19,52.1,75.89,47,75.89,47s-4.6-3.31-8.08-1.75c0.57-0.77,1.11-1.56,1.58-2.4C69.88,42.03,70.31,41.18,70.71,40.31z"/>
</svg>
<foreignObject width="230" height="35" style="font-size: 14px;color: rgb(67, 39, 135);font-family: Arial;font-weight: 700;text-align: left;letter-spacing: 0em;line-height: 1.5;" x="64" y="24">
<div xmlns="http://www.w3.org/1999/xhtml">#1 Repository Of The Day</div>
</foreignObject>
<foreignObject width="141" height="36" style="font-size: 18px;color: rgb(74, 14, 153);font-family: Arial;font-weight: 400;text-align: center;letter-spacing: 0em;line-height: 1.5;" x="-36" y="9">
<div xmlns="http://www.w3.org/1999/xhtml">1</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+25 -56
View File
@@ -10,7 +10,7 @@ description: "Common issues and solutions for Claude-Mem"
Describe any issues you're experiencing to Claude, and the troubleshoot skill will automatically activate to provide diagnosis and fixes.
The troubleshoot skill will:
- ✅ Check PM2 worker status and health
- ✅ Check worker status and health
- ✅ Verify database existence and integrity
- ✅ Test worker service connectivity
- ✅ Validate dependencies installation
@@ -170,39 +170,18 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
4. Restart Claude Code after manual install
### PM2 ENOENT Error on Windows (v5.1.1 Fix)
**Symptoms**: Worker fails to start with "ENOENT" error on Windows.
**Solutions**:
1. This was fixed in v5.1.1 - update to latest version:
```bash
/plugin update claude-mem
```
2. If still experiencing issues, verify PM2 path:
```bash
cd ~/.claude/plugins/marketplaces/thedotmack
dir node_modules\.bin\pm2.cmd
```
3. Manual PM2 install if needed:
```bash
npm install pm2@latest
```
## Worker Service Issues
### Worker Service Not Starting
**Symptoms**: Worker doesn't start, or `pm2 status` shows no processes.
**Symptoms**: Worker doesn't start, or worker status shows it's not running.
**Solutions**:
1. Check if PM2 is running:
1. Check worker status:
```bash
pm2 status
npm run worker:status
```
2. Try starting manually:
@@ -217,14 +196,14 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
4. Full reset:
```bash
pm2 delete claude-mem-worker
npm run worker:stop
npm run worker:start
```
5. Verify PM2 is installed:
5. Verify Bun is installed:
```bash
which pm2
npm list pm2
which bun
bun --version
```
### Port Allocation Failed
@@ -256,7 +235,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
### Worker Keeps Crashing
**Symptoms**: Worker restarts repeatedly, PM2 shows high restart count.
**Symptoms**: Worker restarts repeatedly or fails to stay running.
**Solutions**:
@@ -265,23 +244,21 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
npm run worker:logs
```
2. Check memory usage:
2. Check worker status:
```bash
pm2 status
npm run worker:status
```
3. Increase memory limit in `ecosystem.config.cjs`:
```javascript
{
max_memory_restart: '2G' // Increase if needed
}
```
4. Check database for corruption:
3. Check database for corruption:
```bash
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
```
4. Verify Bun installation:
```bash
bun --version
```
### Worker Not Processing Observations
**Symptoms**: Observations saved but not processed, no summaries generated.
@@ -424,7 +401,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
1. Close all connections:
```bash
pm2 stop claude-mem-worker
npm run worker:stop
```
2. Check for stale locks:
@@ -542,7 +519,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
2. Verify search server is built:
```bash
ls -l plugin/scripts/search-server.js
ls -l plugin/scripts/mcp-server.cjs
```
3. Rebuild if needed:
@@ -656,29 +633,21 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
### High Memory Usage
**Symptoms**: Worker uses too much memory, frequent restarts.
**Symptoms**: Worker uses too much memory.
**Solutions**:
1. Check current usage:
```bash
pm2 status
npm run worker:status
```
2. Increase memory limit:
```javascript
// In ecosystem.config.cjs
{
max_memory_restart: '2G'
}
```
3. Restart worker:
2. Restart worker:
```bash
npm run worker:restart
```
4. Clean up old data (see "Database Too Large" above)
3. Clean up old data (see "Database Too Large" above)
## Installation Issues
@@ -773,10 +742,10 @@ sqlite3 ~/.claude-mem/claude-mem.db "
```bash
# Check if worker is running
pm2 status
npm run worker:status
# View logs
pm2 logs claude-mem-worker
npm run worker:logs
# Check port file
cat ~/.claude-mem/worker.port
+169
View File
@@ -0,0 +1,169 @@
---
title: Claude Desktop Skill
description: Use claude-mem memory search in Claude Desktop with the mem-search skill
icon: desktop
---
<Note>
**Availability:** The mem-search skill works with Claude Desktop on macOS and Windows.
</Note>
## Overview
Claude Desktop can access your claude-mem memory database through the **mem-search** skill. This allows you to search past sessions, decisions, and observations directly from Claude Desktop conversations.
## Prerequisites
Before installing the skill, ensure:
1. **claude-mem is installed** and the worker service is running
2. **MCP server is configured** in Claude Desktop (the skill uses the `mem-search` MCP server)
### Verify Worker is Running
```bash
curl http://localhost:37777/api/health
# Should return: {"status":"ok"}
```
## Installation
### Step 1: Download the Skill
Download the skill package from the repository:
<Card title="mem-search.zip" icon="download" href="https://github.com/thedotmack/claude-mem/raw/main/desktop-skill/mem-search.zip">
Download the mem-search skill for Claude Desktop
</Card>
Or build from source:
```bash
cd desktop-skill
zip -r mem-search.zip Skill.md
```
### Step 2: Install in Claude Desktop
1. Open **Claude Desktop**
2. Go to **Settings** (gear icon)
3. Navigate to **Skills**
4. Click **Install Skill** or drag the `mem-search.zip` file
5. Confirm installation
### Step 3: Configure MCP Server
The skill requires the `mem-search` MCP server. Add this to your Claude Desktop configuration:
<Tabs>
<Tab title="macOS">
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"mem-search": {
"command": "node",
"args": [
"/Users/YOUR_USERNAME/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs"
]
}
}
}
```
</Tab>
<Tab title="Windows">
Edit `%APPDATA%\Claude\claude_desktop_config.json`:
```json
{
"mcpServers": {
"mem-search": {
"command": "node",
"args": [
"C:\\Users\\YOUR_USERNAME\\.claude\\plugins\\marketplaces\\thedotmack\\plugin\\scripts\\mcp-server.cjs"
]
}
}
}
```
</Tab>
</Tabs>
<Warning>
Replace `YOUR_USERNAME` with your actual username. Restart Claude Desktop after editing the configuration.
</Warning>
### Step 4: Restart Claude Desktop
Close and reopen Claude Desktop for the MCP server configuration to take effect.
## Usage
Once installed, the skill auto-activates when you ask about past work:
```
"What did we do last session?"
"Did we fix this bug before?"
"How did we implement authentication?"
"What decisions did we make about the API?"
"Show me changes to worker-service.ts"
```
## Available Search Tools
The skill provides access to these MCP tools:
| Tool | Description |
|------|-------------|
| `search` | Unified search across all memory types |
| `decisions` | Find architectural/design decisions |
| `changes` | Find code changes and refactorings |
| `timeline` | Get observations around a specific point in time |
| `find_by_file` | Find observations for specific files |
| `find_by_type` | Filter by type (decision, bugfix, feature, refactor, discovery, change) |
| `find_by_concept` | Find by concept tags |
| `how_it_works` | Understand system architecture and design patterns |
## Troubleshooting
### Skill Not Appearing
1. Verify the zip file was properly installed
2. Check Claude Desktop's skill installation logs
3. Restart Claude Desktop
### MCP Server Connection Failed
1. Verify the worker is running: `curl http://localhost:37777/api/health`
2. Check the MCP server path in configuration
3. Look for errors in Claude Desktop logs
<Tabs>
<Tab title="macOS">
```bash
# View Claude Desktop logs
tail -f ~/Library/Logs/Claude/claude.log
```
</Tab>
<Tab title="Windows">
Check `%APPDATA%\Claude\logs\`
</Tab>
</Tabs>
### Search Returns No Results
1. Ensure claude-mem has recorded sessions (check http://localhost:37777)
2. Verify the database exists: `ls ~/.claude-mem/claude-mem.db`
3. Test the API directly: `curl "http://localhost:37777/api/search?query=test"`
## Related
<CardGroup cols={2}>
<Card title="Search Tools" icon="magnifying-glass" href="/usage/search-tools">
Complete search API reference
</Card>
<Card title="Platform Integration" icon="plug" href="/platform-integration">
Build custom integrations
</Card>
</CardGroup>
+295
View File
@@ -0,0 +1,295 @@
---
title: "Memory Export/Import"
description: "Share knowledge across claude-mem installations with duplicate prevention"
---
# Memory Export/Import Scripts
Share your claude-mem knowledge with other users! These scripts allow you to export specific memories (observations, sessions, summaries, and prompts) and import them into another claude-mem installation.
## Use Cases
- **Share Windows compatibility knowledge** with Windows users
- **Share bug fix patterns** with contributors
- **Share project-specific learnings** across teams
- **Backup specific memory sets** for safekeeping
## How It Works
### Export Script
Searches the database using **hybrid search** (combines ChromaDB vector embeddings with FTS5 full-text search) and exports all matching:
- **Observations** - Individual learnings and discoveries
- **Sessions** - Session metadata
- **Summaries** - Session summaries
- **Prompts** - User prompts that led to the work
Output is a portable JSON file that can be shared.
> **Privacy Note:** Export files contain all matching memory data in plain text. Review exports before sharing to ensure no sensitive information (API keys, passwords, private paths) is included.
### Import Script
Imports memories with **duplicate prevention**:
- Checks if each record already exists before inserting
- Skips duplicates automatically
- Maintains data integrity with transactional imports
- Reports what was imported vs. skipped
**Duplicate Detection Strategy:**
- **Sessions**: By `claude_session_id` (unique)
- **Summaries**: By `sdk_session_id` (unique)
- **Observations**: By `sdk_session_id` + `title` + `created_at_epoch` (composite)
- **Prompts**: By `claude_session_id` + `prompt_number` (composite)
## Usage
### Export Memories
```bash
# Export all Windows-related memories
npx tsx scripts/export-memories.ts "windows" windows-memories.json
# Export bug fixes
npx tsx scripts/export-memories.ts "bugfix" bugfixes.json
# Export specific feature work
npx tsx scripts/export-memories.ts "progressive disclosure" progressive-disclosure.json
```
**Parameters:**
1. `<query>` - Search query (uses hybrid semantic + full-text search)
2. `<output-file>` - Output JSON file path
3. `--project=name` - Optional: filter results to a specific project
**Example Output:**
```
🔍 Searching for: "windows"
✅ Found 54 observations
✅ Found 12 sessions
✅ Found 12 summaries
✅ Found 7 prompts
📦 Export complete!
📄 Output: windows-memories.json
📊 Stats:
• 54 observations
• 12 sessions
• 12 summaries
• 7 prompts
```
### Import Memories
```bash
# Import from an export file
npx tsx scripts/import-memories.ts windows-memories.json
```
**Parameters:**
1. `<input-file>` - Input JSON file (from export script)
**Example Output:**
```
📦 Import file: windows-memories.json
📅 Exported: 2025-12-10T23:45:00.000Z
🔍 Query: "windows"
📊 Contains:
• 54 observations
• 12 sessions
• 12 summaries
• 7 prompts
🔄 Importing sessions...
✅ Imported: 12, Skipped: 0
🔄 Importing summaries...
✅ Imported: 12, Skipped: 0
🔄 Importing observations...
✅ Imported: 54, Skipped: 0
🔄 Importing prompts...
✅ Imported: 7, Skipped: 0
✅ Import complete!
📊 Summary:
Sessions: 12 imported, 0 skipped
Summaries: 12 imported, 0 skipped
Observations: 54 imported, 0 skipped
Prompts: 7 imported, 0 skipped
```
### Re-importing (Duplicate Prevention)
If you run the import again on the same file, duplicates are automatically skipped:
```
🔄 Importing sessions...
✅ Imported: 0, Skipped: 12 ← All skipped (already exist)
🔄 Importing summaries...
✅ Imported: 0, Skipped: 12
🔄 Importing observations...
✅ Imported: 0, Skipped: 54
🔄 Importing prompts...
✅ Imported: 0, Skipped: 7
```
## Sharing Memories
### For Export Authors
1. **Export your memories:**
```bash
npx tsx scripts/export-memories.ts "windows" windows-memories.json
```
2. **Share the JSON file** via:
- GitHub gist
- Project repository (`shared-memories/`)
- Direct file transfer
- Package in releases
3. **Document what's included:**
- What query was used
- What knowledge is contained
- Who might benefit from it
### For Import Users
1. **Download the export file** to your local machine
2. **Review what's in it** (optional):
```bash
cat windows-memories.json | jq '.totalObservations, .totalSessions'
```
3. **Import into your database:**
```bash
npx tsx scripts/import-memories.ts windows-memories.json
```
4. **Verify import** by searching:
```bash
curl "http://localhost:37777/api/search?query=windows&format=index&limit=10"
```
## JSON Export Format
```json
{
"exportedAt": "2025-12-10T23:45:00.000Z",
"exportedAtEpoch": 1733876700000,
"query": "windows",
"totalObservations": 54,
"totalSessions": 12,
"totalSummaries": 12,
"totalPrompts": 7,
"observations": [ /* array of observation objects */ ],
"sessions": [ /* array of session objects */ ],
"summaries": [ /* array of summary objects */ ],
"prompts": [ /* array of prompt objects */ ]
}
```
## Safety Features
✅ **Duplicate Prevention** - Won't re-import existing records
✅ **Transactional** - All-or-nothing imports (database stays consistent)
✅ **Read-only Export** - Export script opens database in read-only mode
✅ **Dependency Ordering** - Sessions imported before observations/summaries
✅ **Validation** - Checks database exists before starting
## Advanced Usage
### Export by Project
```bash
# Export only claude-mem project memories
npx tsx scripts/export-memories.ts "bugfix" bugfixes.json --project=claude-mem
# Export all memories for a specific project
npx tsx scripts/export-memories.ts "" all-project.json --project=my-app
```
### Export by Type
```bash
# Export only discoveries
npx tsx scripts/export-memories.ts "type:discovery" discoveries.json
# Export only bug fixes
npx tsx scripts/export-memories.ts "type:bugfix" bugfixes.json
```
### Export by Date Range
You can filter the export after exporting:
```bash
# Export all memories, then filter manually with jq
npx tsx scripts/export-memories.ts "" all-memories.json
cat all-memories.json | jq '.observations |= map(select(.created_at_epoch > 1700000000000))' > recent-memories.json
```
### Combine Multiple Exports
```bash
# Export different topics
npx tsx scripts/export-memories.ts "windows" windows.json
npx tsx scripts/export-memories.ts "linux" linux.json
# Import both
npx tsx scripts/import-memories.ts windows.json
npx tsx scripts/import-memories.ts linux.json
```
## Troubleshooting
### Database Not Found
```
❌ Database not found at: /Users/you/.claude-mem/claude-mem.db
```
**Solution:** Make sure claude-mem is installed and has been run at least once.
### Import File Not Found
```
❌ Input file not found: windows-memories.json
```
**Solution:** Check the file path. Use absolute paths if needed.
### Partial Import
If import fails mid-way, the transaction is rolled back - your database remains unchanged. Fix the issue and try again.
## Contributing Memory Sets
If you've exported valuable knowledge that others might benefit from:
1. Create a PR to the `shared-memories/` directory
2. Include a README describing what's in the export
3. Tag with relevant keywords (windows, linux, bugfix, etc.)
4. Community members can then import your knowledge!
## Examples of Useful Exports
**Windows Compatibility Knowledge:**
```bash
npx tsx scripts/export-memories.ts "windows compatibility installation" windows-fixes.json
```
**Progressive Disclosure Architecture:**
```bash
npx tsx scripts/export-memories.ts "progressive disclosure architecture token" pd-patterns.json
```
**Bug Fix Patterns:**
```bash
npx tsx scripts/export-memories.ts "bugfix error handling" bugfix-patterns.json
```
**Performance Optimization:**
```bash
npx tsx scripts/export-memories.ts "performance optimization caching" perf-tips.json
```
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: "Private Tags"
description: "Control what gets stored in memory with <private> tags"
description: "Control what gets stored in memory with privacy tags"
---
# Private Tags
@@ -175,7 +175,7 @@ This design ensures that private content never reaches the database, search indi
1. Verify correct syntax: `<private>content</private>`
2. Check `~/.claude-mem/silent.log` for errors
3. Ensure worker is running: `pm2 list`
3. Ensure worker is running: `npm run worker:status`
4. Restart worker: `npm run worker:restart`
### Partial Content Stored
+1 -1
View File
@@ -364,7 +364,7 @@ search_sessions with query="[YOUR PROJECT NAME]" and orderBy="date_desc"
If search isn't working, check the worker service:
```bash
pm2 list # Check worker status
npm run worker:status # Check worker status
npm run worker:restart # Restart if needed
npm run worker:logs # View logs
```
-38
View File
@@ -1,38 +0,0 @@
/**
* PM2 Ecosystem Configuration for claude-mem Worker Service
*
* Usage:
* pm2 start ecosystem.config.cjs
* pm2 stop claude-mem-worker
* pm2 restart claude-mem-worker
* pm2 logs claude-mem-worker
* pm2 status
*/
module.exports = {
apps: [
{
name: 'claude-mem-worker',
script: './plugin/scripts/worker-service.cjs',
// INTENTIONAL: Watch mode enables auto-restart on plugin updates
//
// Why this is enabled:
// - When you run `npm run sync-marketplace` or rebuild the plugin,
// files in ~/.claude/plugins/marketplaces/thedotmack/ change
// - Watch mode detects these changes and auto-restarts the worker
// - Users get the latest code without manually running `pm2 restart`
//
// This is a feature, not a bug - it ensures users always run the
// latest version after plugin updates.
watch: true,
ignore_watch: [
'node_modules',
'logs',
'*.log',
'*.db',
'*.db-*',
'.git'
]
}
]
};
-5855
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.0.1",
"version": "7.1.11",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
@@ -27,7 +27,8 @@
},
"type": "module",
"engines": {
"node": ">=18.0.0"
"node": ">=18.0.0",
"bun": ">=1.0.0"
},
"scripts": {
"build": "node scripts/build-hooks.js",
@@ -37,30 +38,29 @@
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js",
"sync-marketplace": "node scripts/sync-marketplace.cjs",
"sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
"worker:start": "pm2 start ecosystem.config.cjs",
"worker:stop": "pm2 stop claude-mem-worker",
"worker:restart": "pm2 restart claude-mem-worker",
"worker:logs": "pm2 flush claude-mem-worker && pm2 logs claude-mem-worker --lines 100 --nostream",
"worker:logs:no-flush": "pm2 logs claude-mem-worker --lines 100 --nostream",
"build:binaries": "node scripts/build-worker-binary.js",
"worker:start": "bun plugin/scripts/worker-cli.js start",
"worker:stop": "bun plugin/scripts/worker-cli.js stop",
"worker:restart": "bun plugin/scripts/worker-cli.js restart",
"worker:status": "bun plugin/scripts/worker-cli.js status",
"worker:logs": "tail -f ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
"changelog:generate": "node scripts/generate-changelog.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": "npx tsx scripts/translate-readme/cli.ts -v README.md zh ko ja"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
"@anthropic-ai/claude-agent-sdk": "^0.1.67",
"@modelcontextprotocol/sdk": "^1.20.1",
"ansi-to-html": "^0.7.2",
"better-sqlite3": "^12.5.0",
"express": "^4.18.2",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
"pm2": "^6.0.13",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zod-to-json-schema": "^3.24.6"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.8",
"@types/cors": "^2.8.19",
"@types/express": "^4.17.21",
"@types/node": "^20.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.0.1",
"version": "7.1.11",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+6 -6
View File
@@ -7,12 +7,12 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && node \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js\"",
"timeout": 300
},
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js\"",
"timeout": 10
}
]
@@ -23,7 +23,7 @@
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js\"",
"timeout": 120
}
]
@@ -35,7 +35,7 @@
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js\"",
"timeout": 120
}
]
@@ -46,7 +46,7 @@
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js\"",
"timeout": 120
}
]
@@ -57,7 +57,7 @@
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js\"",
"timeout": 120
}
]
+12
View File
@@ -0,0 +1,12 @@
{
"name": "claude-mem-plugin",
"version": "7.1.10",
"private": true,
"description": "Runtime dependencies for claude-mem bundled hooks",
"type": "module",
"dependencies": {},
"engines": {
"node": ">=18.0.0",
"bun": ">=1.0.0"
}
}
-111
View File
@@ -1,111 +0,0 @@
#!/bin/bash
# claude-mem-settings.sh - User settings manager for claude-mem plugin
USER_SETTINGS_FILE="$HOME/.claude/settings.json"
# Function to check if jq is available
check_jq() {
if ! command -v jq &> /dev/null; then
echo "Error: jq is required for JSON manipulation"
echo "Install with: brew install jq"
exit 1
fi
}
# Function to create settings file if it doesn't exist
ensure_settings_file() {
if [ ! -f "$USER_SETTINGS_FILE" ]; then
mkdir -p "$(dirname "$USER_SETTINGS_FILE")"
echo '{}' > "$USER_SETTINGS_FILE"
fi
}
# Function to get current model setting
get_model() {
if [ -f "$USER_SETTINGS_FILE" ]; then
jq -r '.env.CLAUDE_MEM_MODEL // "claude-sonnet-4-5"' "$USER_SETTINGS_FILE"
else
echo "claude-sonnet-4-5"
fi
}
# Function to set model setting
set_model() {
local model=$1
ensure_settings_file
# Update or create the env.CLAUDE_MEM_MODEL setting
jq --arg model "$model" '.env.CLAUDE_MEM_MODEL = $model' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE"
echo "Set CLAUDE_MEM_MODEL to: $model"
}
# Function to remove model setting
remove_model() {
if [ -f "$USER_SETTINGS_FILE" ]; then
jq 'del(.env.CLAUDE_MEM_MODEL)' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE"
echo "Removed CLAUDE_MEM_MODEL (will use default: claude-sonnet-4-5)"
fi
}
# Function to list available models
list_models() {
echo "Available models:"
echo " claude-haiku-4-5 - Fast and efficient"
echo " claude-sonnet-4-5 - Balanced (default)"
echo " claude-opus-4 - Most capable"
echo " claude-3-7-sonnet - Alternative version"
}
# Interactive menu
show_menu() {
echo "Claude Mem Plugin - Model Configuration"
echo "======================================"
echo "Current model: $(get_model)"
echo "Settings file: $USER_SETTINGS_FILE"
echo ""
echo "1) Set model"
echo "2) Remove model setting (use default)"
echo "3) List available models"
echo "4) Exit"
echo ""
}
# Main interactive loop
main() {
check_jq
while true; do
show_menu
read -p "Choose an option (1-4): " choice
case $choice in
1)
list_models
echo ""
read -p "Enter model name: " model
set_model "$model"
;;
2)
remove_model
;;
3)
list_models
;;
4)
echo "Goodbye!"
exit 0
;;
*)
echo "Invalid option. Please choose 1-4."
;;
esac
echo ""
read -p "Press Enter to continue..."
done
}
# Run main if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
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
File diff suppressed because one or more lines are too long
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env node
/**
* Smart Install Script for claude-mem
*
* Ensures Bun runtime and uv (Python package manager) are installed
* (auto-installs if missing) and handles dependency installation when needed.
*/
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { execSync, spawnSync } from 'child_process';
import { join } from 'path';
import { homedir } from 'os';
const ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const MARKER = join(ROOT, '.install-version');
const IS_WINDOWS = process.platform === 'win32';
/**
* Check if Bun is installed and accessible
*/
function isBunInstalled() {
try {
const result = spawnSync('bun', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
if (result.status === 0) return true;
} catch {
// PATH check failed, try common installation paths
}
// Check common installation paths (handles fresh installs before PATH reload)
const bunPaths = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
return bunPaths.some(existsSync);
}
/**
* Get the Bun executable path (from PATH or common install locations)
*/
function getBunPath() {
// Try PATH first
try {
const result = spawnSync('bun', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
if (result.status === 0) return 'bun';
} catch {
// Not in PATH
}
// Check common installation paths
const bunPaths = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
for (const bunPath of bunPaths) {
if (existsSync(bunPath)) return bunPath;
}
return null;
}
/**
* Get Bun version if installed
*/
function getBunVersion() {
const bunPath = getBunPath();
if (!bunPath) return null;
try {
const result = spawnSync(bunPath, ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
return result.status === 0 ? result.stdout.trim() : null;
} catch {
return null;
}
}
/**
* Check if uv is installed and accessible
*/
function isUvInstalled() {
try {
const result = spawnSync('uv', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
if (result.status === 0) return true;
} catch {
// PATH check failed, try common installation paths
}
// Check common installation paths (handles fresh installs before PATH reload)
const uvPaths = IS_WINDOWS
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
return uvPaths.some(existsSync);
}
/**
* Get uv version if installed
*/
function getUvVersion() {
try {
const result = spawnSync('uv', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
return result.status === 0 ? result.stdout.trim() : null;
} catch {
return null;
}
}
/**
* Install Bun automatically based on platform
*/
function installBun() {
console.error('🔧 Bun not found. Installing Bun runtime...');
try {
if (IS_WINDOWS) {
// Windows: Use PowerShell installer
console.error(' Installing via PowerShell...');
execSync('powershell -c "irm bun.sh/install.ps1 | iex"', {
stdio: 'inherit',
shell: true
});
} else {
// Unix/macOS: Use curl installer
console.error(' Installing via curl...');
execSync('curl -fsSL https://bun.sh/install | bash', {
stdio: 'inherit',
shell: true
});
}
// Verify installation
if (isBunInstalled()) {
const version = getBunVersion();
console.error(`✅ Bun ${version} installed successfully`);
return true;
} else {
// Bun may be installed but not in PATH yet for this session
// Try common installation paths
const bunPaths = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
for (const bunPath of bunPaths) {
if (existsSync(bunPath)) {
console.error(`✅ Bun installed at ${bunPath}`);
console.error('⚠️ Please restart your terminal or add Bun to PATH:');
if (IS_WINDOWS) {
console.error(` $env:Path += ";${join(homedir(), '.bun', 'bin')}"`);
} else {
console.error(` export PATH="$HOME/.bun/bin:$PATH"`);
}
return true;
}
}
throw new Error('Bun installation completed but binary not found');
}
} catch (error) {
console.error('❌ Failed to install Bun automatically');
console.error(' Please install manually:');
if (IS_WINDOWS) {
console.error(' - winget install Oven-sh.Bun');
console.error(' - Or: powershell -c "irm bun.sh/install.ps1 | iex"');
} else {
console.error(' - curl -fsSL https://bun.sh/install | bash');
console.error(' - Or: brew install oven-sh/bun/bun');
}
console.error(' Then restart your terminal and try again.');
throw error;
}
}
/**
* Install uv automatically based on platform
*/
function installUv() {
console.error('🐍 Installing uv for Python/Chroma support...');
try {
if (IS_WINDOWS) {
// Windows: Use PowerShell installer
console.error(' Installing via PowerShell...');
execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', {
stdio: 'inherit',
shell: true
});
} else {
// Unix/macOS: Use curl installer
console.error(' Installing via curl...');
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
stdio: 'inherit',
shell: true
});
}
// Verify installation
if (isUvInstalled()) {
const version = getUvVersion();
console.error(`✅ uv ${version} installed successfully`);
return true;
} else {
// uv may be installed but not in PATH yet for this session
// Try common installation paths
const uvPaths = IS_WINDOWS
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
for (const uvPath of uvPaths) {
if (existsSync(uvPath)) {
console.error(`✅ uv installed at ${uvPath}`);
console.error('⚠️ Please restart your terminal or add uv to PATH:');
if (IS_WINDOWS) {
console.error(` $env:Path += ";${join(homedir(), '.local', 'bin')}"`);
} else {
console.error(` export PATH="$HOME/.local/bin:$PATH"`);
}
return true;
}
}
throw new Error('uv installation completed but binary not found');
}
} catch (error) {
console.error('❌ Failed to install uv automatically');
console.error(' Please install manually:');
if (IS_WINDOWS) {
console.error(' - winget install astral-sh.uv');
console.error(' - Or: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"');
} else {
console.error(' - curl -LsSf https://astral.sh/uv/install.sh | sh');
console.error(' - Or: brew install uv (macOS)');
}
console.error(' Then restart your terminal and try again.');
throw error;
}
}
/**
* Check if dependencies need to be installed
*/
function needsInstall() {
if (!existsSync(join(ROOT, 'node_modules'))) return true;
try {
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const marker = JSON.parse(readFileSync(MARKER, 'utf-8'));
return pkg.version !== marker.version || getBunVersion() !== marker.bun;
} catch {
return true;
}
}
/**
* Install dependencies using Bun with npm fallback
*
* Bun has issues with npm alias packages (e.g., string-width-cjs, strip-ansi-cjs)
* that are defined in package-lock.json. When bun fails with 404 errors for these
* packages, we fall back to npm which handles aliases correctly.
*/
function installDeps() {
const bunPath = getBunPath();
if (!bunPath) {
throw new Error('Bun executable not found');
}
console.error('📦 Installing dependencies with Bun...');
// Quote path for Windows paths with spaces
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
let bunSucceeded = false;
try {
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
bunSucceeded = true;
} catch {
// First attempt failed, try with force flag
try {
execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
bunSucceeded = true;
} catch {
// Bun failed completely, will try npm fallback
}
}
// Fallback to npm if bun failed (handles npm alias packages correctly)
if (!bunSucceeded) {
console.error('⚠️ Bun install failed, falling back to npm...');
console.error(' (This can happen with npm alias packages like *-cjs)');
try {
execSync('npm install', { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
} catch (npmError) {
throw new Error('Both bun and npm install failed: ' + npmError.message);
}
}
// Write version marker
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
writeFileSync(MARKER, JSON.stringify({
version: pkg.version,
bun: getBunVersion(),
uv: getUvVersion(),
installedAt: new Date().toISOString()
}));
}
// Main execution
try {
// Step 1: Ensure Bun is installed (REQUIRED)
if (!isBunInstalled()) {
installBun();
// Re-check after installation
if (!isBunInstalled()) {
console.error('❌ Bun is required but not available in PATH');
console.error(' Please restart your terminal after installation');
process.exit(1);
}
}
// Step 2: Ensure uv is installed (REQUIRED for vector search)
if (!isUvInstalled()) {
installUv();
// Re-check after installation
if (!isUvInstalled()) {
console.error('❌ uv is required but not available in PATH');
console.error(' Please restart your terminal after installation');
process.exit(1);
}
}
// Step 3: Install dependencies if needed
if (needsInstall()) {
installDeps();
console.error('✅ Dependencies installed');
}
} catch (e) {
console.error('❌ Installation failed:', e.message);
process.exit(1);
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -87,7 +87,7 @@ Quick fixes for frequently encountered claude-mem problems.
**Fix:**
1. Check the observation count setting:
```bash
grep CLAUDE_MEM_CONTEXT_OBSERVATIONS ~/.claude/settings.json
grep CLAUDE_MEM_CONTEXT_OBSERVATIONS ~/.claude-mem/settings.json
```
2. Default is 50 observations - you can adjust this:
@@ -6,7 +6,7 @@ SQLite database troubleshooting for claude-mem.
Claude-mem uses SQLite3 for persistent storage:
- **Location:** `~/.claude-mem/claude-mem.db`
- **Library:** better-sqlite3 (synchronous, not bun:sqlite)
- **Library:** bun:sqlite (native Bun SQLite, synchronous)
- **Features:** FTS5 full-text search, triggers, indexes
- **Tables:** observations, sessions, user_prompts, observations_fts, sessions_fts, prompts_fts
@@ -97,7 +97,6 @@ cd ~/.claude/plugins/marketplaces/thedotmack/
# Check for critical packages
ls node_modules/@anthropic-ai/claude-agent-sdk 2>&1 | head -1
ls node_modules/better-sqlite3 2>&1 | head -1
ls node_modules/express 2>&1 | head -1
ls node_modules/pm2 2>&1 | head -1
```
@@ -188,7 +187,7 @@ echo " Health check: $(curl -s http://127.0.0.1:37777/health 2>/dev/null || ec
echo ""
echo "5. Configuration"
echo " Port setting: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_WORKER_PORT || echo 'default (37777)')"
echo " Observation count: $(cat ~/.claude/settings.json 2>/dev/null | grep CLAUDE_MEM_CONTEXT_OBSERVATIONS || echo 'default (50)')"
echo " Observation count: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_CONTEXT_OBSERVATIONS || echo 'default (50)')"
echo ""
echo "6. Recent Activity"
echo " Latest observation: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT created_at FROM observations ORDER BY created_at DESC LIMIT 1;' 2>/dev/null || echo 'N/A')"
@@ -85,7 +85,7 @@ cat ~/.claude/settings.json
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
# Change context observation count
# Edit ~/.claude/settings.json and add:
# Edit ~/.claude-mem/settings.json and add:
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "25"
@@ -187,7 +187,6 @@ pm2 delete claude-mem-worker
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
ls node_modules/@anthropic-ai/claude-agent-sdk
ls node_modules/better-sqlite3
ls node_modules/express
ls node_modules/pm2
```
File diff suppressed because one or more lines are too long
+55 -7
View File
@@ -36,6 +36,11 @@ const CONTEXT_GENERATOR = {
source: 'src/services/context-generator.ts'
};
const WORKER_CLI = {
name: 'worker-cli',
source: 'src/cli/worker-cli.ts'
};
async function buildHooks() {
console.log('🔨 Building claude-mem hooks and worker service...\n');
@@ -58,6 +63,24 @@ async function buildHooks() {
}
console.log('✓ Output directories ready');
// Generate plugin/package.json for cache directory dependency installation
// Note: bun:sqlite is a Bun built-in, no external dependencies needed for SQLite
console.log('\n📦 Generating plugin package.json...');
const pluginPackageJson = {
name: 'claude-mem-plugin',
version: version,
private: true,
description: 'Runtime dependencies for claude-mem bundled hooks',
type: 'module',
dependencies: {},
engines: {
node: '>=18.0.0',
bun: '>=1.0.0'
}
};
fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n');
console.log('✓ plugin/package.json generated');
// Build React viewer
console.log('\n📋 Building React viewer...');
const { spawn } = await import('child_process');
@@ -83,12 +106,12 @@ async function buildHooks() {
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
minify: true,
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
external: ['better-sqlite3'],
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env node'
js: '#!/usr/bin/env bun'
}
});
@@ -108,12 +131,12 @@ async function buildHooks() {
outfile: `${hooksDir}/${MCP_SERVER.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['better-sqlite3'],
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env node'
js: '#!/usr/bin/env bun'
}
});
@@ -133,7 +156,7 @@ async function buildHooks() {
outfile: `${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['better-sqlite3'],
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
}
@@ -142,6 +165,31 @@ async function buildHooks() {
const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
console.log(`✓ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`);
// Build worker CLI
console.log(`\n🔧 Building worker CLI...`);
await build({
entryPoints: [WORKER_CLI.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'esm',
outfile: `${hooksDir}/${WORKER_CLI.name}.js`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make worker CLI executable
fs.chmodSync(`${hooksDir}/${WORKER_CLI.name}.js`, 0o755);
const workerCliStats = fs.statSync(`${hooksDir}/${WORKER_CLI.name}.js`);
console.log(`✓ worker-cli built (${(workerCliStats.size / 1024).toFixed(2)} KB)`);
// Build each hook
for (const hook of HOOKS) {
console.log(`\n🔧 Building ${hook.name}...`);
@@ -156,12 +204,12 @@ async function buildHooks() {
format: 'esm',
outfile,
minify: true,
external: ['better-sqlite3'],
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env node'
js: '#!/usr/bin/env bun'
}
});
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
/**
* Build Windows executable for claude-mem worker service
* Uses Bun's compile feature to create a standalone exe
*/
import { execSync } from 'child_process';
import fs from 'fs';
const version = JSON.parse(fs.readFileSync('package.json', 'utf-8')).version;
const outDir = 'dist/binaries';
fs.mkdirSync(outDir, { recursive: true });
console.log(`Building Windows exe v${version}...`);
try {
execSync(
`bun build --compile --minify --target=bun-windows-x64 ./src/services/worker-service.ts --outfile ${outDir}/worker-service-v${version}-win-x64.exe`,
{ stdio: 'inherit' }
);
console.log(`\nBuilt: ${outDir}/worker-service-v${version}-win-x64.exe`);
} catch (error) {
console.error('Failed to build Windows binary:', error.message);
process.exit(1);
}
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env node
/**
* Export memories matching a search query to a portable JSON format
* Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]
* Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem
*/
import Database from 'better-sqlite3';
import { existsSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
interface ObservationRecord {
id: number;
sdk_session_id: string;
project: string;
text: string | null;
type: string;
title: string;
subtitle: string | null;
facts: string | null;
narrative: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
interface SdkSessionRecord {
id: number;
claude_session_id: string;
sdk_session_id: string;
project: string;
user_prompt: string;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: string;
}
interface SessionSummaryRecord {
id: number;
sdk_session_id: string;
project: string;
request: string | null;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
files_read: string | null;
files_edited: string | null;
notes: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
interface UserPromptRecord {
id: number;
claude_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
created_at_epoch: number;
}
interface ExportData {
exportedAt: string;
exportedAtEpoch: number;
query: string;
project?: string;
totalObservations: number;
totalSessions: number;
totalSummaries: number;
totalPrompts: number;
observations: ObservationRecord[];
sessions: SdkSessionRecord[];
summaries: SessionSummaryRecord[];
prompts: UserPromptRecord[];
}
async function exportMemories(query: string, outputFile: string, project?: string) {
try {
// Read port from settings
const settings = SettingsDefaultsManager.loadFromFile(join(homedir(), '.claude-mem', 'settings.json'));
const port = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
const baseUrl = `http://localhost:${port}`;
console.log(`🔍 Searching for: "${query}"${project ? ` (project: ${project})` : ' (all projects)'}`);
// Build query params - use format=json for raw data
const params = new URLSearchParams({
query,
format: 'json',
limit: '999999'
});
if (project) params.set('project', project);
// Unified search - gets all result types using hybrid search
console.log('📡 Fetching all memories via hybrid search...');
const searchResponse = await fetch(`${baseUrl}/api/search?${params.toString()}`);
if (!searchResponse.ok) {
throw new Error(`Failed to search: ${searchResponse.status} ${searchResponse.statusText}`);
}
const searchData = await searchResponse.json();
const observations: ObservationRecord[] = searchData.observations || [];
const summaries: SessionSummaryRecord[] = searchData.sessions || [];
const prompts: UserPromptRecord[] = searchData.prompts || [];
console.log(`✅ Found ${observations.length} observations`);
console.log(`✅ Found ${summaries.length} session summaries`);
console.log(`✅ Found ${prompts.length} user prompts`);
// Get unique SDK session IDs from observations and summaries
const sdkSessionIds = new Set<string>();
observations.forEach((o) => {
if (o.sdk_session_id) sdkSessionIds.add(o.sdk_session_id);
});
summaries.forEach((s) => {
if (s.sdk_session_id) sdkSessionIds.add(s.sdk_session_id);
});
// Get SDK sessions metadata from database
// (We need this because the API doesn't expose sdk_sessions table directly)
console.log('📡 Fetching SDK sessions metadata...');
const sessions: SdkSessionRecord[] = [];
if (sdkSessionIds.size > 0) {
// Read directly from database for sdk_sessions table
const Database = (await import('better-sqlite3')).default;
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
if (!existsSync(dbPath)) {
console.error(`❌ Database not found at: ${dbPath}`);
console.error('💡 Has claude-mem been initialized? Try running a session first.');
process.exit(1);
}
const db = new Database(dbPath, { readonly: true });
try {
const placeholders = Array.from(sdkSessionIds).map(() => '?').join(',');
const sessionQuery = `
SELECT * FROM sdk_sessions
WHERE sdk_session_id IN (${placeholders})
ORDER BY started_at_epoch DESC
`;
sessions.push(...db.prepare(sessionQuery).all(...Array.from(sdkSessionIds)));
} finally {
db.close();
}
}
console.log(`✅ Found ${sessions.length} SDK sessions`);
// Create export data
const exportData: ExportData = {
exportedAt: new Date().toISOString(),
exportedAtEpoch: Date.now(),
query,
project,
totalObservations: observations.length,
totalSessions: sessions.length,
totalSummaries: summaries.length,
totalPrompts: prompts.length,
observations,
sessions,
summaries,
prompts
};
// Write to file
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
console.log(`\n📦 Export complete!`);
console.log(`📄 Output: ${outputFile}`);
console.log(`📊 Stats:`);
console.log(`${exportData.totalObservations} observations`);
console.log(`${exportData.totalSessions} sessions`);
console.log(`${exportData.totalSummaries} summaries`);
console.log(`${exportData.totalPrompts} prompts`);
} catch (error) {
console.error('❌ Export failed:', error);
process.exit(1);
}
}
// CLI interface
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]');
console.error('Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem');
console.error(' npx tsx scripts/export-memories.ts "authentication" auth.json');
process.exit(1);
}
// Parse arguments
const [query, outputFile, ...flags] = args;
const project = flags.find(f => f.startsWith('--project='))?.split('=')[1];
exportMemories(query, outputFile, project);
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Find Silent Failure Patterns
#
# This script searches for defensive OR patterns (|| '' || null || undefined)
# that should potentially use happy_path_error__with_fallback instead.
#
# Usage: ./scripts/find-silent-failures.sh
echo "=================================================="
echo "Searching for defensive OR patterns in src/"
echo "These MAY be silent failures that should log errors"
echo "=================================================="
echo ""
echo "🔍 Searching for: || ''"
echo "---"
grep -rn "|| ''" src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "🔍 Searching for: || \"\""
echo "---"
grep -rn '|| ""' src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "🔍 Searching for: || null"
echo "---"
grep -rn "|| null" src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "🔍 Searching for: || undefined"
echo "---"
grep -rn "|| undefined" src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "=================================================="
echo "Review each match and determine if it should use:"
echo " happy_path_error__with_fallback('description', data, fallback)"
echo "=================================================="
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env node
/**
* Import memories from a JSON export file with duplicate prevention
* Usage: npx tsx scripts/import-memories.ts <input-file>
* Example: npx tsx scripts/import-memories.ts windows-memories.json
*/
import Database from 'better-sqlite3';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
interface ImportStats {
sessionsImported: number;
sessionsSkipped: number;
summariesImported: number;
summariesSkipped: number;
observationsImported: number;
observationsSkipped: number;
promptsImported: number;
promptsSkipped: number;
}
function importMemories(inputFile: string) {
if (!existsSync(inputFile)) {
console.error(`❌ Input file not found: ${inputFile}`);
process.exit(1);
}
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
if (!existsSync(dbPath)) {
console.error(`❌ Database not found at: ${dbPath}`);
process.exit(1);
}
// Read and parse export file
const exportData = JSON.parse(readFileSync(inputFile, 'utf-8'));
console.log(`📦 Import file: ${inputFile}`);
console.log(`📅 Exported: ${exportData.exportedAt}`);
console.log(`🔍 Query: "${exportData.query}"`);
console.log(`📊 Contains:`);
console.log(`${exportData.totalObservations} observations`);
console.log(`${exportData.totalSessions} sessions`);
console.log(`${exportData.totalSummaries} summaries`);
console.log(`${exportData.totalPrompts} prompts`);
console.log('');
const db = new Database(dbPath);
const stats: ImportStats = {
sessionsImported: 0,
sessionsSkipped: 0,
summariesImported: 0,
summariesSkipped: 0,
observationsImported: 0,
observationsSkipped: 0,
promptsImported: 0,
promptsSkipped: 0
};
try {
// Prepare statements for duplicate checking
const checkSession = db.prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?');
const checkSummary = db.prepare('SELECT id FROM session_summaries WHERE sdk_session_id = ?');
const checkObservation = db.prepare(`
SELECT id FROM observations
WHERE sdk_session_id = ?
AND title = ?
AND created_at_epoch = ?
`);
const checkPrompt = db.prepare(`
SELECT id FROM user_prompts
WHERE claude_session_id = ?
AND prompt_number = ?
`);
// Prepare insert statements
const insertSession = db.prepare(`
INSERT INTO sdk_sessions (
claude_session_id, sdk_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertSummary = db.prepare(`
INSERT INTO session_summaries (
sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertObservation = db.prepare(`
INSERT INTO observations (
sdk_session_id, project, text, type, title, subtitle,
facts, narrative, concepts, files_read, files_modified,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertPrompt = db.prepare(`
INSERT INTO user_prompts (
claude_session_id, prompt_number, prompt_text,
created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?)
`);
// Import in transaction
db.transaction(() => {
// 1. Import sessions first (dependency for everything else)
console.log('🔄 Importing sessions...');
for (const session of exportData.sessions) {
const exists = checkSession.get(session.claude_session_id);
if (exists) {
stats.sessionsSkipped++;
continue;
}
insertSession.run(
session.claude_session_id,
session.sdk_session_id,
session.project,
session.user_prompt,
session.started_at,
session.started_at_epoch,
session.completed_at,
session.completed_at_epoch,
session.status
);
stats.sessionsImported++;
}
console.log(` ✅ Imported: ${stats.sessionsImported}, Skipped: ${stats.sessionsSkipped}`);
// 2. Import summaries (depends on sessions)
console.log('🔄 Importing summaries...');
for (const summary of exportData.summaries) {
const exists = checkSummary.get(summary.sdk_session_id);
if (exists) {
stats.summariesSkipped++;
continue;
}
insertSummary.run(
summary.sdk_session_id,
summary.project,
summary.request,
summary.investigated,
summary.learned,
summary.completed,
summary.next_steps,
summary.files_read,
summary.files_edited,
summary.notes,
summary.prompt_number,
summary.discovery_tokens || 0,
summary.created_at,
summary.created_at_epoch
);
stats.summariesImported++;
}
console.log(` ✅ Imported: ${stats.summariesImported}, Skipped: ${stats.summariesSkipped}`);
// 3. Import observations (depends on sessions)
console.log('🔄 Importing observations...');
for (const obs of exportData.observations) {
const exists = checkObservation.get(
obs.sdk_session_id,
obs.title,
obs.created_at_epoch
);
if (exists) {
stats.observationsSkipped++;
continue;
}
insertObservation.run(
obs.sdk_session_id,
obs.project,
obs.text,
obs.type,
obs.title,
obs.subtitle,
obs.facts,
obs.narrative,
obs.concepts,
obs.files_read,
obs.files_modified,
obs.prompt_number,
obs.discovery_tokens || 0,
obs.created_at,
obs.created_at_epoch
);
stats.observationsImported++;
}
console.log(` ✅ Imported: ${stats.observationsImported}, Skipped: ${stats.observationsSkipped}`);
// 4. Import prompts (depends on sessions)
console.log('🔄 Importing prompts...');
for (const prompt of exportData.prompts) {
const exists = checkPrompt.get(
prompt.claude_session_id,
prompt.prompt_number
);
if (exists) {
stats.promptsSkipped++;
continue;
}
insertPrompt.run(
prompt.claude_session_id,
prompt.prompt_number,
prompt.prompt_text,
prompt.created_at,
prompt.created_at_epoch
);
stats.promptsImported++;
}
console.log(` ✅ Imported: ${stats.promptsImported}, Skipped: ${stats.promptsSkipped}`);
})();
console.log('\n✅ Import complete!');
console.log('📊 Summary:');
console.log(` Sessions: ${stats.sessionsImported} imported, ${stats.sessionsSkipped} skipped`);
console.log(` Summaries: ${stats.summariesImported} imported, ${stats.summariesSkipped} skipped`);
console.log(` Observations: ${stats.observationsImported} imported, ${stats.observationsSkipped} skipped`);
console.log(` Prompts: ${stats.promptsImported} imported, ${stats.promptsSkipped} skipped`);
} finally {
db.close();
}
}
// CLI interface
const args = process.argv.slice(2);
if (args.length < 1) {
console.error('Usage: npx tsx scripts/import-memories.ts <input-file>');
console.error('Example: npx tsx scripts/import-memories.ts windows-memories.json');
process.exit(1);
}
const [inputFile] = args;
importMemories(inputFile);
+211 -355
View File
@@ -1,403 +1,259 @@
#!/usr/bin/env node
/**
* Smart Install Script for claude-mem
*
* Features:
* - Only runs npm install when necessary (version change or missing deps)
* - Caches installation state with version marker
* - Provides helpful Windows-specific error messages
* - Cross-platform compatible (pure Node.js)
* - Fast when already installed (just version check)
* Ensures Bun runtime and uv (Python package manager) are installed
* (auto-installs if missing) and handles dependency installation when needed.
*/
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { execSync, spawnSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { join } from 'path';
import { homedir } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const MARKER = join(ROOT, '.install-version');
const IS_WINDOWS = process.platform === 'win32';
// Plugin root is parent directory of scripts/
const PLUGIN_ROOT = join(__dirname, '..');
const PACKAGE_JSON_PATH = join(PLUGIN_ROOT, 'package.json');
const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version');
const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules');
const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3');
// Common installation paths (handles fresh installs before PATH reload)
const BUN_COMMON_PATHS = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
// Colors for output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
dim: '\x1b[2m',
};
const UV_COMMON_PATHS = IS_WINDOWS
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
function log(message, color = colors.reset) {
console.error(`${color}${message}${colors.reset}`);
/**
* Get the Bun executable path (from PATH or common install locations)
*/
function getBunPath() {
// Try PATH first
try {
const result = spawnSync('bun', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
if (result.status === 0) return 'bun';
} catch {
// Not in PATH
}
// Check common installation paths
return BUN_COMMON_PATHS.find(existsSync) || null;
}
function getPackageVersion() {
/**
* Check if Bun is installed and accessible
*/
function isBunInstalled() {
return getBunPath() !== null;
}
/**
* Get Bun version if installed
*/
function getBunVersion() {
const bunPath = getBunPath();
if (!bunPath) return null;
try {
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf-8'));
return packageJson.version;
} catch (error) {
log(`⚠️ Failed to read package.json: ${error.message}`, colors.yellow);
const result = spawnSync(bunPath, ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
return result.status === 0 ? result.stdout.trim() : null;
} catch {
return null;
}
}
function getNodeVersion() {
return process.version; // e.g., "v22.21.1"
}
function getInstalledVersion() {
/**
* Get the uv executable path (from PATH or common install locations)
*/
function getUvPath() {
// Try PATH first
try {
if (existsSync(VERSION_MARKER_PATH)) {
const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim();
// Try parsing as JSON (new format)
try {
const marker = JSON.parse(content);
return {
packageVersion: marker.packageVersion,
nodeVersion: marker.nodeVersion,
installedAt: marker.installedAt
};
} catch {
// Fallback: old format (plain text version string)
return {
packageVersion: content,
nodeVersion: null, // Unknown
installedAt: null
};
}
}
} catch (error) {
// Marker doesn't exist or can't be read
}
return null;
}
function setInstalledVersion(packageVersion, nodeVersion) {
try {
const marker = {
packageVersion,
nodeVersion,
installedAt: new Date().toISOString()
};
writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf-8');
} catch (error) {
log(`⚠️ Failed to write version marker: ${error.message}`, colors.yellow);
}
}
function needsInstall() {
// Check if node_modules exists
if (!existsSync(NODE_MODULES_PATH)) {
log('📦 Dependencies not found - first time setup', colors.cyan);
return true;
const result = spawnSync('uv', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
if (result.status === 0) return 'uv';
} catch {
// Not in PATH
}
// Check if better-sqlite3 is installed
if (!existsSync(BETTER_SQLITE3_PATH)) {
log('📦 better-sqlite3 missing - reinstalling', colors.cyan);
return true;
}
// Check version marker
const currentPackageVersion = getPackageVersion();
const currentNodeVersion = getNodeVersion();
const installed = getInstalledVersion();
if (!installed) {
log('📦 No version marker found - installing', colors.cyan);
return true;
}
// Check package version
if (currentPackageVersion !== installed.packageVersion) {
log(`📦 Version changed (${installed.packageVersion}${currentPackageVersion}) - updating`, colors.cyan);
return true;
}
// Check Node.js version
if (installed.nodeVersion && currentNodeVersion !== installed.nodeVersion) {
log(`📦 Node.js version changed (${installed.nodeVersion}${currentNodeVersion}) - rebuilding native modules`, colors.cyan);
return true;
}
// If old format (no nodeVersion), assume needs install
if (!installed.nodeVersion) {
log('📦 Old version marker format - updating', colors.cyan);
return true;
}
// All good - no install needed
log(`✓ Dependencies already installed (v${currentPackageVersion})`, colors.dim);
return false;
// Check common installation paths
return UV_COMMON_PATHS.find(existsSync) || null;
}
/**
* Verify that better-sqlite3 native module loads correctly
* This catches ABI mismatches and corrupted builds
* Check if uv is installed and accessible
*/
async function verifyNativeModules() {
function isUvInstalled() {
return getUvPath() !== null;
}
/**
* Get uv version if installed
*/
function getUvVersion() {
const uvPath = getUvPath();
if (!uvPath) return null;
try {
log('🔍 Verifying native modules...', colors.dim);
// Try to actually load better-sqlite3
const { default: Database } = await import('better-sqlite3');
// Try to create a test in-memory database
const db = new Database(':memory:');
// Run a simple query to ensure it works
const result = db.prepare('SELECT 1 + 1 as result').get();
// Clean up
db.close();
if (result.result !== 2) {
throw new Error('SQLite math check failed');
}
log('✓ Native modules verified', colors.dim);
return true;
} catch (error) {
if (error.code === 'ERR_DLOPEN_FAILED') {
log('⚠️ Native module ABI mismatch detected', colors.yellow);
return false;
}
// Other errors are unexpected - log and fail
log(`❌ Native module verification failed: ${error.message}`, colors.red);
return false;
const result = spawnSync(uvPath, ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: IS_WINDOWS
});
return result.status === 0 ? result.stdout.trim() : null;
} catch {
return null;
}
}
function getWindowsErrorHelp(errorOutput) {
// Detect Python version at runtime
let pythonStatus = ' Python not detected or version unknown';
/**
* Install Bun automatically based on platform
*/
function installBun() {
console.error('🔧 Bun not found. Installing Bun runtime...');
try {
const pythonVersion = execSync('python --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
const versionMatch = pythonVersion.match(/Python\s+([\d.]+)/);
if (versionMatch) {
pythonStatus = ` You have ${versionMatch[0]} installed ✓`;
}
} catch (error) {
// Python not available or failed to detect - use default message
}
const help = [
'',
'╔══════════════════════════════════════════════════════════════════════╗',
'║ Windows Installation Help ║',
'╚══════════════════════════════════════════════════════════════════════╝',
'',
'📋 better-sqlite3 requires build tools to compile native modules.',
'',
'🔧 Option 1: Install Visual Studio Build Tools (Recommended)',
' 1. Download: https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
' 2. Install "Desktop development with C++"',
' 3. Restart your terminal',
' 4. Try again',
'',
'🔧 Option 2: Install via npm (automated)',
' Run as Administrator:',
' npm install --global windows-build-tools',
'',
'🐍 Python Requirement:',
' Python 3.6+ is required.',
pythonStatus,
'',
];
// Check for specific error patterns
if (errorOutput.includes('MSBuild.exe')) {
help.push('❌ MSBuild not found - install Visual Studio Build Tools');
}
if (errorOutput.includes('MSVS')) {
help.push('❌ Visual Studio not detected - install Build Tools');
}
if (errorOutput.includes('permission') || errorOutput.includes('EPERM')) {
help.push('❌ Permission denied - try running as Administrator');
}
help.push('');
help.push('📖 Full documentation: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md');
help.push('');
return help.join('\n');
}
async function runNpmInstall() {
const isWindows = process.platform === 'win32';
log('', colors.cyan);
log('🔨 Installing dependencies...', colors.bright);
log('', colors.reset);
// Try normal install first, then retry with force if it fails
const strategies = [
{ command: 'npm install', label: 'normal' },
{ command: 'npm install --force', label: 'with force flag' },
];
let lastError = null;
for (const { command, label } of strategies) {
try {
log(`Attempting install ${label}...`, colors.dim);
// Run npm install silently
execSync(command, {
cwd: PLUGIN_ROOT,
stdio: 'pipe', // Silent output unless error
encoding: 'utf-8',
if (IS_WINDOWS) {
console.error(' Installing via PowerShell...');
execSync('powershell -c "irm bun.sh/install.ps1 | iex"', {
stdio: 'inherit',
shell: true
});
} else {
console.error(' Installing via curl...');
execSync('curl -fsSL https://bun.sh/install | bash', {
stdio: 'inherit',
shell: true
});
// Verify better-sqlite3 was installed
if (!existsSync(BETTER_SQLITE3_PATH)) {
throw new Error('better-sqlite3 installation verification failed');
}
// NEW: Verify native modules actually work
const nativeModulesWork = await verifyNativeModules();
if (!nativeModulesWork) {
throw new Error('Native modules failed to load after install');
}
const packageVersion = getPackageVersion();
const nodeVersion = getNodeVersion();
setInstalledVersion(packageVersion, nodeVersion);
log('', colors.green);
log('✅ Dependencies installed successfully!', colors.bright);
log(` Package version: ${packageVersion}`, colors.dim);
log(` Node.js version: ${nodeVersion}`, colors.dim);
log('', colors.reset);
return true;
} catch (error) {
lastError = error;
// Continue to next strategy
}
}
// All strategies failed - show error
log('', colors.red);
log('❌ Installation failed after retrying!', colors.bright);
log('', colors.reset);
// Provide Windows-specific help
if (isWindows && lastError && lastError.message && lastError.message.includes('better-sqlite3')) {
log(getWindowsErrorHelp(lastError.message), colors.yellow);
}
// Show generic error info with troubleshooting steps
if (lastError) {
if (lastError.stderr) {
log('Error output:', colors.dim);
log(lastError.stderr.toString(), colors.red);
} else if (lastError.message) {
log(lastError.message, colors.red);
}
log('', colors.yellow);
log('📋 Troubleshooting Steps:', colors.bright);
log('', colors.reset);
log('1. Check your internet connection', colors.yellow);
log('2. Try running: npm cache clean --force', colors.yellow);
log('3. Try running: npm install (in plugin directory)', colors.yellow);
log('4. Check npm version: npm --version (requires npm 7+)', colors.yellow);
log('5. Try updating npm: npm install -g npm@latest', colors.yellow);
log('', colors.reset);
}
if (!isBunInstalled()) {
throw new Error(
'Bun installation completed but binary not found. ' +
'Please restart your terminal and try again.'
);
}
return false;
const version = getBunVersion();
console.error(`✅ Bun ${version} installed successfully`);
} catch (error) {
console.error('❌ Failed to install Bun');
console.error(' Please install manually:');
if (IS_WINDOWS) {
console.error(' - winget install Oven-sh.Bun');
console.error(' - Or: powershell -c "irm bun.sh/install.ps1 | iex"');
} else {
console.error(' - curl -fsSL https://bun.sh/install | bash');
console.error(' - Or: brew install oven-sh/bun/bun');
}
console.error(' Then restart your terminal and try again.');
throw error;
}
}
/**
* Check if we should fail when worker startup fails
* Returns true if worker failed AND dependencies are missing
* Install uv automatically based on platform
*/
function shouldFailOnWorkerStartup(workerStarted) {
return !workerStarted && !existsSync(NODE_MODULES_PATH);
}
function installUv() {
console.error('🐍 Installing uv for Python/Chroma support...');
async function main() {
try {
// Check if we need to install dependencies
const installNeeded = needsInstall();
if (installNeeded) {
// Run installation (now async)
const installSuccess = await runNpmInstall();
if (!installSuccess) {
log('', colors.red);
log('⚠️ Installation failed', colors.yellow);
log('', colors.reset);
process.exit(1);
}
if (IS_WINDOWS) {
console.error(' Installing via PowerShell...');
execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', {
stdio: 'inherit',
shell: true
});
} else {
// NEW: Even if install not needed, verify native modules work
const nativeModulesWork = await verifyNativeModules();
if (!nativeModulesWork) {
log('📦 Native modules need rebuild - reinstalling', colors.cyan);
const installSuccess = await runNpmInstall();
if (!installSuccess) {
log('', colors.red);
log('⚠️ Native module rebuild failed', colors.yellow);
log('', colors.reset);
process.exit(1);
}
}
console.error(' Installing via curl...');
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
stdio: 'inherit',
shell: true
});
}
// Try to start the PM2 worker after fresh install
try {
log('🚀 Starting worker service...', colors.cyan);
// On Windows, PM2 executable is pm2.cmd, not pm2
const localPm2Base = join(NODE_MODULES_PATH, '.bin', 'pm2');
const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base;
const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2';
const ecosystemPath = join(PLUGIN_ROOT, 'ecosystem.config.cjs');
// Using spawnSync with array args to avoid command injection risks
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: PLUGIN_ROOT,
stdio: 'pipe',
encoding: 'utf-8'
});
if (result.status !== 0) {
throw new Error(result.stderr || 'PM2 start failed');
}
log('✅ Worker service started', colors.green);
} catch (error) {
// Worker might already be running or PM2 not available - that's okay
// The ensureWorkerRunning() function will handle auto-start when needed
log('️ Worker will start automatically when needed', colors.dim);
}
// Success - dependencies installed (if needed)
process.exit(0);
if (!isUvInstalled()) {
throw new Error(
'uv installation completed but binary not found. ' +
'Please restart your terminal and try again.'
);
}
const version = getUvVersion();
console.error(`✅ uv ${version} installed successfully`);
} catch (error) {
log(`❌ Unexpected error: ${error.message}`, colors.red);
log('', colors.reset);
process.exit(1);
console.error('❌ Failed to install uv');
console.error(' Please install manually:');
if (IS_WINDOWS) {
console.error(' - winget install astral-sh.uv');
console.error(' - Or: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"');
} else {
console.error(' - curl -LsSf https://astral.sh/uv/install.sh | sh');
console.error(' - Or: brew install uv (macOS)');
}
console.error(' Then restart your terminal and try again.');
throw error;
}
}
main();
/**
* Check if dependencies need to be installed
*/
function needsInstall() {
if (!existsSync(join(ROOT, 'node_modules'))) return true;
try {
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const marker = JSON.parse(readFileSync(MARKER, 'utf-8'));
return pkg.version !== marker.version || getBunVersion() !== marker.bun;
} catch {
return true;
}
}
/**
* Install dependencies using Bun
*/
function installDeps() {
const bunPath = getBunPath();
if (!bunPath) {
throw new Error('Bun executable not found');
}
console.error('📦 Installing dependencies with Bun...');
// Quote path for Windows paths with spaces
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
// Write version marker
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
writeFileSync(MARKER, JSON.stringify({
version: pkg.version,
bun: getBunVersion(),
uv: getUvVersion(),
installedAt: new Date().toISOString()
}));
}
// Main execution
try {
if (!isBunInstalled()) installBun();
if (!isUvInstalled()) installUv();
if (needsInstall()) {
installDeps();
console.error('✅ Dependencies installed');
}
} catch (e) {
console.error('❌ Installation failed:', e.message);
process.exit(1);
}
+27 -1
View File
@@ -61,7 +61,7 @@ function getPluginVersion() {
console.log('Syncing to marketplace...');
try {
execSync(
'rsync -av --delete --exclude=.git ./ ~/.claude/plugins/marketplaces/thedotmack/',
'rsync -av --delete --exclude=.git --exclude=/.mcp.json ./ ~/.claude/plugins/marketplaces/thedotmack/',
{ stdio: 'inherit' }
);
@@ -82,6 +82,32 @@ try {
);
console.log('\x1b[32m%s\x1b[0m', 'Sync complete!');
// Trigger worker restart after file sync
console.log('\n🔄 Triggering worker restart...');
const http = require('http');
const req = http.request({
hostname: '127.0.0.1',
port: 37777,
path: '/api/admin/restart',
method: 'POST',
timeout: 2000
}, (res) => {
if (res.statusCode === 200) {
console.log('\x1b[32m%s\x1b[0m', '✓ Worker restart triggered');
} else {
console.log('\x1b[33m%s\x1b[0m', ` Worker restart returned status ${res.statusCode}`);
}
});
req.on('error', () => {
console.log('\x1b[33m%s\x1b[0m', ' Worker not running, will start on next hook');
});
req.on('timeout', () => {
req.destroy();
console.log('\x1b[33m%s\x1b[0m', ' Worker restart timed out');
});
req.end();
} catch (error) {
console.error('\x1b[31m%s\x1b[0m', 'Sync failed:', error.message);
process.exit(1);
+235
View File
@@ -0,0 +1,235 @@
# README Translator
Translate README.md files to multiple languages using the Claude Agent SDK. Perfect for build scripts and CI/CD pipelines.
## Installation
```bash
npm install readme-translator
# or
npm install -g readme-translator # for CLI usage
```
## Requirements
- Node.js 18+
- **Authentication** (one of the following):
- Claude Code installed and authenticated (Pro/Max subscription) - **no API key needed**
- `ANTHROPIC_API_KEY` environment variable set (for API-based usage)
- AWS Bedrock (`CLAUDE_CODE_USE_BEDROCK=1` + AWS credentials)
- Google Vertex AI (`CLAUDE_CODE_USE_VERTEX=1` + GCP credentials)
If you have Claude Code installed and logged in with your Pro/Max subscription, the SDK will automatically use that authentication.
## CLI Usage
```bash
# Basic usage
translate-readme README.md es fr de
# With options
translate-readme -v -o ./i18n --pattern docs.{lang}.md README.md es fr de ja zh
# List supported languages
translate-readme --list-languages
```
### CLI Options
| Option | Description |
|--------|-------------|
| `-o, --output <dir>` | Output directory (default: same as source) |
| `-p, --pattern <pat>` | Output filename pattern (default: `README.{lang}.md`) |
| `--no-preserve-code` | Translate code blocks too (not recommended) |
| `-m, --model <model>` | Claude model to use (default: `sonnet`) |
| `--max-budget <usd>` | Maximum budget in USD |
| `-v, --verbose` | Show detailed progress |
| `-h, --help` | Show help message |
| `--list-languages` | List all supported language codes |
## Programmatic Usage
```typescript
import { translateReadme } from "readme-translator";
const result = await translateReadme({
source: "./README.md",
languages: ["es", "fr", "de", "ja", "zh"],
verbose: true,
});
console.log(`Translated ${result.successful} files`);
console.log(`Total cost: $${result.totalCostUsd.toFixed(4)}`);
```
### API Options
```typescript
interface TranslationOptions {
/** Source README file path */
source: string;
/** Target language codes */
languages: string[];
/** Output directory (defaults to same directory as source) */
outputDir?: string;
/** Output filename pattern (use {lang} placeholder) */
pattern?: string; // default: "README.{lang}.md"
/** Preserve code blocks without translation */
preserveCode?: boolean; // default: true
/** Claude model to use */
model?: string; // default: "sonnet"
/** Maximum budget in USD */
maxBudgetUsd?: number;
/** Verbose output */
verbose?: boolean;
}
```
### Return Value
```typescript
interface TranslationJobResult {
results: TranslationResult[];
totalCostUsd: number;
successful: number;
failed: number;
}
interface TranslationResult {
language: string;
outputPath: string;
success: boolean;
error?: string;
costUsd?: number;
}
```
## Build Script Integration
### package.json
```json
{
"scripts": {
"translate": "translate-readme README.md es fr de ja zh",
"translate:all": "translate-readme -v -o ./i18n README.md es fr de it pt ja ko zh ru ar",
"prebuild": "npm run translate"
}
}
```
### GitHub Actions
Note: CI/CD environments require an API key since Claude Code won't be authenticated there.
```yaml
name: Translate README
on:
push:
branches: [main]
paths: [README.md]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g readme-translator
- name: Translate README
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
translate-readme -v -o ./i18n README.md es fr de ja zh
- name: Commit translations
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add i18n/
git diff --staged --quiet || git commit -m "chore: update README translations"
git push
```
### Programmatic Build Script
```typescript
// scripts/translate.ts
import { translateReadme } from "readme-translator";
async function main() {
const result = await translateReadme({
source: "./README.md",
languages: (process.env.TRANSLATE_LANGS || "es,fr,de").split(","),
outputDir: "./docs/i18n",
maxBudgetUsd: 5.0,
verbose: !process.env.CI,
});
if (result.failed > 0) {
console.error("Some translations failed");
process.exit(1);
}
}
main();
```
## Supported Languages
| Code | Language | Code | Language |
|------|----------|------|----------|
| `ar` | Arabic | `ko` | Korean |
| `bg` | Bulgarian | `lt` | Lithuanian |
| `cs` | Czech | `lv` | Latvian |
| `da` | Danish | `nl` | Dutch |
| `de` | German | `no` | Norwegian |
| `el` | Greek | `pl` | Polish |
| `es` | Spanish | `pt` | Portuguese |
| `et` | Estonian | `pt-br` | Brazilian Portuguese |
| `fi` | Finnish | `ro` | Romanian |
| `fr` | French | `ru` | Russian |
| `he` | Hebrew | `sk` | Slovak |
| `hi` | Hindi | `sl` | Slovenian |
| `hu` | Hungarian | `sv` | Swedish |
| `id` | Indonesian | `th` | Thai |
| `it` | Italian | `tr` | Turkish |
| `ja` | Japanese | `uk` | Ukrainian |
| | | `vi` | Vietnamese |
| | | `zh` | Chinese (Simplified) |
| | | `zh-tw` | Chinese (Traditional) |
## Best Practices
1. **Preserve Code Blocks**: Keep `preserveCode: true` (default) to avoid breaking code examples
2. **Set Budget Limits**: Use `maxBudgetUsd` to prevent runaway costs
3. **Run on Releases Only**: In CI/CD, trigger translations only on main branch or releases
4. **Review Translations**: Automated translations are good but not perfect - consider human review for critical docs
5. **Cache Results**: Don't re-translate unchanged content - check if README changed before running
## Cost Estimation
Typical costs per language (varies by README length):
- Short README (~500 words): ~$0.01-0.02
- Medium README (~2000 words): ~$0.05-0.10
- Long README (~5000 words): ~$0.15-0.25
## License
MIT
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env npx tsx
import { translateReadme, SUPPORTED_LANGUAGES } from "./index.ts";
interface CliArgs {
source: string;
languages: string[];
outputDir?: string;
pattern?: string;
preserveCode: boolean;
model?: string;
maxBudget?: number;
verbose: boolean;
help: boolean;
listLanguages: boolean;
}
function printHelp(): void {
console.log(`
readme-translator - Translate README.md files using Claude Agent SDK
AUTHENTICATION:
If Claude Code is installed and authenticated (Pro/Max subscription),
no API key is needed. Otherwise, set ANTHROPIC_API_KEY environment variable.
USAGE:
translate-readme [options] <source> <languages...>
translate-readme --help
translate-readme --list-languages
ARGUMENTS:
source Path to the source README.md file
languages Target language codes (e.g., es fr de ja zh)
OPTIONS:
-o, --output <dir> Output directory (default: same as source)
-p, --pattern <pat> Output filename pattern (default: README.{lang}.md)
--no-preserve-code Translate code blocks too (not recommended)
-m, --model <model> Claude model to use (default: sonnet)
--max-budget <usd> Maximum budget in USD
-v, --verbose Show detailed progress
-h, --help Show this help message
--list-languages List all supported language codes
EXAMPLES:
# Translate to Spanish and French
translate-readme README.md es fr
# Translate to multiple languages with custom output
translate-readme -v -o ./i18n --pattern docs.{lang}.md README.md de ja ko zh
# Use in npm scripts
# package.json: "translate": "translate-readme README.md es fr de"
SUPPORTED LANGUAGES:
Run with --list-languages to see all supported language codes
`);
}
function printLanguages(): void {
const LANGUAGE_NAMES: Record<string, string> = {
ar: "Arabic",
bg: "Bulgarian",
cs: "Czech",
da: "Danish",
de: "German",
el: "Greek",
es: "Spanish",
et: "Estonian",
fi: "Finnish",
fr: "French",
he: "Hebrew",
hi: "Hindi",
hu: "Hungarian",
id: "Indonesian",
it: "Italian",
ja: "Japanese",
ko: "Korean",
lt: "Lithuanian",
lv: "Latvian",
nl: "Dutch",
no: "Norwegian",
pl: "Polish",
pt: "Portuguese",
"pt-br": "Brazilian Portuguese",
ro: "Romanian",
ru: "Russian",
sk: "Slovak",
sl: "Slovenian",
sv: "Swedish",
th: "Thai",
tr: "Turkish",
uk: "Ukrainian",
vi: "Vietnamese",
zh: "Chinese (Simplified)",
"zh-tw": "Chinese (Traditional)",
};
console.log("\nSupported Language Codes:\n");
const sorted = Object.entries(LANGUAGE_NAMES).sort((a, b) =>
a[1].localeCompare(b[1])
);
for (const [code, name] of sorted) {
console.log(` ${code.padEnd(8)} ${name}`);
}
console.log("");
}
function parseArgs(argv: string[]): CliArgs {
const args: CliArgs = {
source: "",
languages: [],
preserveCode: true,
verbose: false,
help: false,
listLanguages: false,
};
const positional: string[] = [];
let i = 2; // Skip node and script path
while (i < argv.length) {
const arg = argv[i];
switch (arg) {
case "-h":
case "--help":
args.help = true;
break;
case "--list-languages":
args.listLanguages = true;
break;
case "-v":
case "--verbose":
args.verbose = true;
break;
case "--no-preserve-code":
args.preserveCode = false;
break;
case "-o":
case "--output":
args.outputDir = argv[++i];
break;
case "-p":
case "--pattern":
args.pattern = argv[++i];
break;
case "-m":
case "--model":
args.model = argv[++i];
break;
case "--max-budget":
args.maxBudget = parseFloat(argv[++i]);
break;
default:
if (arg.startsWith("-")) {
console.error(`Unknown option: ${arg}`);
process.exit(1);
}
positional.push(arg);
}
i++;
}
if (positional.length > 0) {
args.source = positional[0];
args.languages = positional.slice(1);
}
return args;
}
async function main(): Promise<void> {
const args = parseArgs(process.argv);
if (args.help) {
printHelp();
process.exit(0);
}
if (args.listLanguages) {
printLanguages();
process.exit(0);
}
if (!args.source) {
console.error("Error: No source file specified");
console.error("Run with --help for usage information");
process.exit(1);
}
if (args.languages.length === 0) {
console.error("Error: No target languages specified");
console.error("Run with --help for usage information");
process.exit(1);
}
// Validate language codes
const invalidLangs = args.languages.filter(
(lang) => !SUPPORTED_LANGUAGES.includes(lang.toLowerCase())
);
if (invalidLangs.length > 0) {
console.error(`Error: Unknown language codes: ${invalidLangs.join(", ")}`);
console.error("Run with --list-languages to see supported codes");
process.exit(1);
}
try {
const result = await translateReadme({
source: args.source,
languages: args.languages,
outputDir: args.outputDir,
pattern: args.pattern,
preserveCode: args.preserveCode,
model: args.model,
maxBudgetUsd: args.maxBudget,
verbose: args.verbose,
});
// Exit with error code if any translations failed
if (result.failed > 0) {
process.exit(1);
}
} catch (error) {
console.error(
"Translation failed:",
error instanceof Error ? error.message : error
);
process.exit(1);
}
}
main();
+147
View File
@@ -0,0 +1,147 @@
/**
* Example: Using readme-translator in build scripts
*
* These examples show how to integrate the translator into your build pipeline.
*/
import { translateReadme, TranslationJobResult, SUPPORTED_LANGUAGES } from "./index.js";
// Example 1: Simple usage - translate to a few common languages
async function translateToCommonLanguages(): Promise<void> {
const result = await translateReadme({
source: "./README.md",
languages: ["es", "fr", "de", "ja", "zh"],
verbose: true,
});
console.log(`Translated to ${result.successful} languages`);
}
// Example 2: Full i18n setup with custom output directory
async function fullI18nSetup(): Promise<void> {
const result = await translateReadme({
source: "./README.md",
languages: ["es", "fr", "de", "it", "pt", "ja", "ko", "zh", "ru", "ar"],
outputDir: "./docs/i18n",
pattern: "README.{lang}.md",
preserveCode: true,
model: "sonnet",
maxBudgetUsd: 5.0, // Cap spending at $5
verbose: true,
});
// Handle results programmatically
for (const r of result.results) {
if (!r.success) {
console.error(`Failed to translate to ${r.language}: ${r.error}`);
}
}
}
// Example 3: Build script integration with error handling
// Note: If Claude Code is authenticated, no API key needed locally.
// CI/CD environments will need ANTHROPIC_API_KEY set.
async function buildScriptIntegration(): Promise<number> {
try {
const result = await translateReadme({
source: process.env.README_PATH || "./README.md",
languages: (process.env.TRANSLATE_LANGS || "es,fr,de").split(","),
outputDir: process.env.I18N_OUTPUT || "./i18n",
verbose: process.env.CI !== "true", // Quiet in CI
});
// Return exit code for build scripts
return result.failed > 0 ? 1 : 0;
} catch (error) {
console.error("Translation failed:", error);
return 1;
}
}
// Example 4: Batch translation of multiple READMEs
async function batchTranslation(): Promise<void> {
const readmes = [
"./README.md",
"./packages/core/README.md",
"./packages/cli/README.md",
];
const languages = ["es", "fr", "de"];
for (const readme of readmes) {
console.log(`\nProcessing: ${readme}`);
await translateReadme({
source: readme,
languages,
verbose: true,
});
}
}
// Example 5: Custom output pattern for docs sites
async function docsiteSetup(): Promise<void> {
// For docusaurus/vitepress style: docs/README.es.md
await translateReadme({
source: "./README.md",
languages: ["es", "fr", "de", "ja", "zh"],
outputDir: "./docs",
pattern: "README.{lang}.md",
verbose: true,
});
}
// Example 6: Conditional translation in CI/CD
async function cicdTranslation(): Promise<void> {
// Only translate on main branch releases
const isRelease = process.env.GITHUB_REF === "refs/heads/main";
const isManualTrigger = process.env.GITHUB_EVENT_NAME === "workflow_dispatch";
if (!isRelease && !isManualTrigger) {
console.log("Skipping translation - not a release build");
return;
}
const result = await translateReadme({
source: "./README.md",
languages: ["es", "fr", "de", "ja", "ko", "zh", "pt-br"],
outputDir: "./dist/i18n",
maxBudgetUsd: 10.0,
verbose: true,
});
// Write summary for GitHub Actions
if (process.env.GITHUB_STEP_SUMMARY) {
const summary = `
## Translation Summary
- Successful: ${result.successful}
- Failed: ${result.failed}
- 💰 Cost: $${result.totalCostUsd.toFixed(4)}
`;
// In real usage, write to GITHUB_STEP_SUMMARY
console.log(summary);
}
}
// Run an example
const example = process.argv[2];
switch (example) {
case "simple":
translateToCommonLanguages();
break;
case "full":
fullI18nSetup();
break;
case "batch":
batchTranslation();
break;
case "docs":
docsiteSetup();
break;
case "ci":
cicdTranslation();
break;
default:
console.log("Available examples: simple, full, batch, docs, ci");
console.log("\nSupported languages:", SUPPORTED_LANGUAGES.join(", "));
}
+292
View File
@@ -0,0 +1,292 @@
import { query, type SDKMessage, type SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
import * as fs from "fs/promises";
import * as path from "path";
export interface TranslationOptions {
/** Source README file path */
source: string;
/** Target languages (e.g., ['es', 'fr', 'de', 'ja', 'zh']) */
languages: string[];
/** Output directory (defaults to same directory as source) */
outputDir?: string;
/** Output filename pattern (use {lang} placeholder, defaults to 'README.{lang}.md') */
pattern?: string;
/** Preserve code blocks without translation */
preserveCode?: boolean;
/** Model to use (defaults to 'sonnet') */
model?: string;
/** Maximum budget in USD for the entire translation job */
maxBudgetUsd?: number;
/** Verbose output */
verbose?: boolean;
}
export interface TranslationResult {
language: string;
outputPath: string;
success: boolean;
error?: string;
costUsd?: number;
}
export interface TranslationJobResult {
results: TranslationResult[];
totalCostUsd: number;
successful: number;
failed: number;
}
const LANGUAGE_NAMES: Record<string, string> = {
ar: "Arabic",
bg: "Bulgarian",
cs: "Czech",
da: "Danish",
de: "German",
el: "Greek",
es: "Spanish",
et: "Estonian",
fi: "Finnish",
fr: "French",
he: "Hebrew",
hi: "Hindi",
hu: "Hungarian",
id: "Indonesian",
it: "Italian",
ja: "Japanese",
ko: "Korean",
lt: "Lithuanian",
lv: "Latvian",
nl: "Dutch",
no: "Norwegian",
pl: "Polish",
pt: "Portuguese",
"pt-br": "Brazilian Portuguese",
ro: "Romanian",
ru: "Russian",
sk: "Slovak",
sl: "Slovenian",
sv: "Swedish",
th: "Thai",
tr: "Turkish",
uk: "Ukrainian",
vi: "Vietnamese",
zh: "Chinese (Simplified)",
"zh-tw": "Chinese (Traditional)",
};
function getLanguageName(code: string): string {
return LANGUAGE_NAMES[code.toLowerCase()] || code;
}
async function translateToLanguage(
content: string,
targetLang: string,
options: Pick<TranslationOptions, "preserveCode" | "model" | "verbose">
): Promise<{ translation: string; costUsd: number }> {
const languageName = getLanguageName(targetLang);
const preserveCodeInstructions = options.preserveCode
? `
IMPORTANT: Preserve all code blocks exactly as they are. Do NOT translate:
- Code inside \`\`\` blocks
- Inline code inside \` backticks
- Command examples
- File paths
- Variable names, function names, and technical identifiers
- URLs and links
`
: "";
const prompt = `Translate the following README.md content from English to ${languageName} (${targetLang}).
${preserveCodeInstructions}
Guidelines:
- Maintain all Markdown formatting (headers, lists, links, etc.)
- Keep the same document structure
- Translate headings, descriptions, and explanatory text naturally
- Preserve technical accuracy
- Use appropriate technical terminology for ${languageName}
- Keep proper nouns (product names, company names) unchanged unless they have official translations
Here is the README content to translate:
---
${content}
---
Output ONLY the translated README content, nothing else. Do not include any preamble or explanation.`;
let translation = "";
let costUsd = 0;
let charCount = 0;
const startTime = Date.now();
const stream = query({
prompt,
options: {
model: options.model || "sonnet",
systemPrompt: `You are an expert technical translator specializing in software documentation.
You translate README files while preserving Markdown formatting and technical accuracy.
Always output only the translated content without any surrounding explanation.`,
permissionMode: "bypassPermissions",
allowDangerouslySkipPermissions: true,
includePartialMessages: true, // Enable streaming events
},
});
// Progress spinner frames
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let spinnerIdx = 0;
for await (const message of stream) {
// Handle streaming text deltas
if (message.type === "stream_event") {
const event = message.event as { type: string; delta?: { type: string; text?: string } };
if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
translation += event.delta.text;
charCount += event.delta.text.length;
if (options.verbose) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const spinner = spinnerFrames[spinnerIdx++ % spinnerFrames.length];
process.stdout.write(`\r ${spinner} Translating... ${charCount} chars (${elapsed}s)`);
}
}
}
// Handle full assistant messages (fallback)
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text" && !translation) {
translation = block.text;
charCount = translation.length;
}
}
}
if (message.type === "result") {
const result = message as SDKResultMessage;
if (result.subtype === "success") {
costUsd = result.total_cost_usd;
// Use the result text if we didn't get it from streaming
if (!translation && result.result) {
translation = result.result;
charCount = translation.length;
}
}
}
}
// Clear the progress line
if (options.verbose) {
process.stdout.write("\r" + " ".repeat(60) + "\r");
}
return { translation: translation.trim(), costUsd };
}
export async function translateReadme(
options: TranslationOptions
): Promise<TranslationJobResult> {
const {
source,
languages,
outputDir,
pattern = "README.{lang}.md",
preserveCode = true,
model,
maxBudgetUsd,
verbose = false,
} = options;
// Read source file
const sourcePath = path.resolve(source);
const content = await fs.readFile(sourcePath, "utf-8");
// Determine output directory
const outDir = outputDir ? path.resolve(outputDir) : path.dirname(sourcePath);
await fs.mkdir(outDir, { recursive: true });
const results: TranslationResult[] = [];
let totalCostUsd = 0;
if (verbose) {
console.log(`📖 Source: ${sourcePath}`);
console.log(`📂 Output: ${outDir}`);
console.log(`🌍 Languages: ${languages.join(", ")}`);
console.log("");
}
for (const lang of languages) {
// Check budget
if (maxBudgetUsd && totalCostUsd >= maxBudgetUsd) {
results.push({
language: lang,
outputPath: "",
success: false,
error: "Budget exceeded",
});
continue;
}
const outputFilename = pattern.replace("{lang}", lang);
const outputPath = path.join(outDir, outputFilename);
if (verbose) {
console.log(`🔄 Translating to ${getLanguageName(lang)} (${lang})...`);
}
try {
const { translation, costUsd } = await translateToLanguage(content, lang, {
preserveCode,
model,
verbose,
});
await fs.writeFile(outputPath, translation, "utf-8");
totalCostUsd += costUsd;
results.push({
language: lang,
outputPath,
success: true,
costUsd,
});
if (verbose) {
console.log(` ✅ Saved to ${outputFilename} ($${costUsd.toFixed(4)})`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
results.push({
language: lang,
outputPath,
success: false,
error: errorMessage,
});
if (verbose) {
console.log(` ❌ Failed: ${errorMessage}`);
}
}
}
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
if (verbose) {
console.log("");
console.log(`📊 Summary: ${successful} succeeded, ${failed} failed`);
console.log(`💰 Total cost: $${totalCostUsd.toFixed(4)}`);
}
return {
results,
totalCostUsd,
successful,
failed,
};
}
// Export language codes for convenience
export const SUPPORTED_LANGUAGES = Object.keys(LANGUAGE_NAMES);
+63
View File
@@ -0,0 +1,63 @@
import { ProcessManager } from '../services/process/ProcessManager.js';
import { getWorkerPort } from '../shared/worker-utils.js';
const command = process.argv[2];
const port = getWorkerPort();
async function main() {
switch (command) {
case 'start': {
const result = await ProcessManager.start(port);
if (result.success) {
console.log(`Worker started (PID: ${result.pid})`);
const date = new Date().toISOString().slice(0, 10);
console.log(`Logs: ~/.claude-mem/logs/worker-${date}.log`);
process.exit(0);
} else {
console.error(`Failed to start: ${result.error}`);
process.exit(1);
}
break;
}
case 'stop': {
await ProcessManager.stop();
console.log('Worker stopped');
process.exit(0);
}
case 'restart': {
const result = await ProcessManager.restart(port);
if (result.success) {
console.log(`Worker restarted (PID: ${result.pid})`);
process.exit(0);
} else {
console.error(`Failed to restart: ${result.error}`);
process.exit(1);
}
break;
}
case 'status': {
const status = await ProcessManager.status();
if (status.running) {
console.log('Worker is running');
console.log(` PID: ${status.pid}`);
console.log(` Port: ${status.port}`);
console.log(` Uptime: ${status.uptime}`);
} else {
console.log('Worker is not running');
}
process.exit(0);
}
default:
console.log('Usage: worker-cli.js <start|stop|restart|status>');
process.exit(1);
}
}
main().catch(error => {
console.error(error);
process.exit(1);
});
+8 -21
View File
@@ -8,13 +8,11 @@
import { stdin } from 'process';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
export interface SessionEndInput {
session_id: string;
cwd: string;
transcript_path?: string;
hook_event_name: string;
reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other';
}
@@ -25,24 +23,13 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
silentDebug('[cleanup-hook] Hook fired', {
happy_path_error__with_fallback('[cleanup-hook] Hook fired', {
session_id: input?.session_id,
cwd: input?.cwd,
reason: input?.reason
});
// Handle standalone execution (no input provided)
if (!input) {
console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook');
console.log('\nExpected input format:');
console.log(JSON.stringify({
session_id: "string",
cwd: "string",
transcript_path: "string",
hook_event_name: "SessionEnd",
reason: "exit"
}, null, 2));
process.exit(0);
throw new Error('cleanup-hook requires input from Claude Code');
}
const { session_id, reason } = input;
@@ -58,19 +45,19 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
claudeSessionId: session_id,
reason
}),
signal: AbortSignal.timeout(2000)
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (response.ok) {
const result = await response.json();
silentDebug('[cleanup-hook] Session cleanup completed', result);
happy_path_error__with_fallback('[cleanup-hook] Session cleanup completed', result);
} else {
// Non-fatal - session might not exist
silentDebug('[cleanup-hook] Session not found or already cleaned up');
happy_path_error__with_fallback('[cleanup-hook] Session not found or already cleaned up');
}
} catch (error: any) {
// Worker might not be running - that's okay
silentDebug('[cleanup-hook] Worker not reachable (non-critical)', {
happy_path_error__with_fallback('[cleanup-hook] Worker not reachable (non-critical)', {
error: error.message
});
}
+20 -8
View File
@@ -8,16 +8,16 @@
import path from "path";
import { stdin } from "process";
import { execSync } from "child_process";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
import { handleWorkerError } from "../shared/hook-error-handler.js";
import { getWorkerRestartInstructions } from "../utils/error-messages.js";
export interface SessionStartInput {
session_id?: string;
transcript_path?: string;
cwd?: string;
session_id: string;
transcript_path: string;
cwd: string;
hook_event_name?: string;
source?: "startup" | "resume" | "clear" | "compact";
[key: string]: any;
}
async function contextHook(input?: SessionStartInput): Promise<string> {
@@ -29,8 +29,20 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
const port = getWorkerPort();
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
return result.trim();
try {
const response = await fetch(url, { signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT) });
if (!response.ok) {
const errorText = await response.text();
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
}
const result = await response.text();
return result.trim();
} catch (error: any) {
handleWorkerError(error);
}
}
// Entry Point - handle stdin/stdout
+1 -16
View File
@@ -1,4 +1,4 @@
export type HookType = 'PreCompact' | 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop' | string;
export type HookType = 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop';
export interface HookResponseOptions {
reason?: string;
@@ -20,21 +20,6 @@ function buildHookResponse(
success: boolean,
options: HookResponseOptions
): HookResponse {
if (hookType === 'PreCompact') {
if (success) {
return {
continue: true,
suppressOutput: true
};
}
return {
continue: false,
stopReason: options.reason || 'Pre-compact operation failed',
suppressOutput: true
};
}
if (hookType === 'SessionStart') {
if (success && options.context) {
return {
+47 -96
View File
@@ -1,51 +1,15 @@
/**
* New Hook - UserPromptSubmit
*
* DUAL PURPOSE HOOK: Handles BOTH session initialization AND continuation
* ==========================================================================
*
* CRITICAL ARCHITECTURE FACTS (NEVER FORGET):
*
* 1. SESSION ID THREADING - The Single Source of Truth
* - Claude Code assigns ONE session_id per conversation
* - ALL hooks in that conversation receive the SAME session_id
* - We ALWAYS use this session_id - NEVER generate our own
* - This is how NEW hook, SAVE hook, and SUMMARY hook stay connected
*
* 2. NO EXISTENCE CHECKS NEEDED
* - createSDKSession is idempotent (INSERT OR IGNORE)
* - Prompt #1: Creates new database row, returns new ID
* - Prompt #2+: Row exists, returns existing ID
* - We NEVER need to check "does session exist?" - just use the session_id
*
* 3. CONTINUATION LOGIC LOCATION
* - This hook does NOT contain continuation prompt logic
* - That lives in SDKAgent.ts (lines 125-127)
* - SDKAgent checks promptNumber to choose init vs continuation prompt
* - BOTH prompts receive the SAME session_id from this hook
*
* 4. UNIFIED WITH SAVE HOOK
* - SAVE hook uses: db.createSDKSession(session_id, '', '')
* - NEW hook uses: db.createSDKSession(session_id, project, prompt)
* - Both use session_id from hook context - this keeps everything connected
*
* This is KISS in action: Use the session_id we're given, trust idempotent
* database operations, and let SDKAgent handle init vs continuation logic.
*/
import path from 'path';
import { stdin } from 'process';
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { createHookResponse } from './hook-response.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js';
import { stripMemoryTagsFromPrompt } from '../utils/tag-stripping.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { handleWorkerError } from '../shared/hook-error-handler.js';
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
export interface UserPromptSubmitInput {
session_id: string;
cwd: string;
prompt: string;
[key: string]: any;
}
@@ -61,85 +25,72 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
}
const { session_id, cwd, prompt } = input;
const project = path.basename(cwd);
// Debug: Log what we received
silentDebug('[new-hook] Input received', {
happy_path_error__with_fallback('[new-hook] Input received', {
session_id,
cwd,
cwd_type: typeof cwd,
cwd_length: cwd?.length,
has_cwd: !!cwd,
project,
prompt_length: prompt?.length
});
const project = path.basename(cwd);
silentDebug('[new-hook] Project extracted', {
project,
project_type: typeof project,
project_length: project?.length,
is_empty: project === '',
cwd_was: cwd
});
const db = new SessionStore();
// CRITICAL: Use session_id from hook as THE source of truth
// createSDKSession is idempotent - creates new or returns existing
// This is how ALL hooks stay connected to the same session
const sessionDbId = db.createSDKSession(session_id, project, prompt);
const promptNumber = db.incrementPromptCounter(sessionDbId);
// Strip memory tags before saving user prompt to prevent privacy leaks
// Tags like <private> and <claude-mem-context> should not be stored or searchable
const cleanedUserPrompt = stripMemoryTagsFromPrompt(prompt);
// Skip memory operations for fully private prompts
// If the entire prompt was wrapped in <private> tags, don't create any observations
if (!cleanedUserPrompt || cleanedUserPrompt.trim() === '') {
silentDebug('[new-hook] Prompt entirely private, skipping memory operations', {
session_id,
promptNumber,
originalLength: prompt.length
});
db.close();
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber} (fully private - skipped)`);
console.log(createHookResponse('UserPromptSubmit', true));
return;
}
db.saveUserPrompt(session_id, promptNumber, cleanedUserPrompt);
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
db.close();
const port = getWorkerPort();
// Initialize session via HTTP - handles DB operations and privacy checks
let sessionDbId: number;
let promptNumber: number;
try {
const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: session_id,
project,
prompt
}),
signal: AbortSignal.timeout(5000)
});
if (!initResponse.ok) {
const errorText = await initResponse.text();
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
}
const initResult = await initResponse.json();
sessionDbId = initResult.sessionDbId;
promptNumber = initResult.promptNumber;
// Check if prompt was entirely private (worker performs privacy check)
if (initResult.skipped && initResult.reason === 'private') {
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber} (fully private - skipped)`);
console.log(createHookResponse('UserPromptSubmit', true));
return;
}
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
} catch (error: any) {
handleWorkerError(error);
}
// Strip leading slash from commands for memory agent
// /review 101 → review 101 (more semantic for observations)
const cleanedPrompt = prompt.startsWith('/') ? prompt.substring(1) : prompt;
try {
// Initialize session via HTTP
// Initialize SDK agent session via HTTP (starts the agent!)
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, userPrompt: cleanedPrompt, promptNumber }),
body: JSON.stringify({ userPrompt: cleanedPrompt, promptNumber }),
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
}
} catch (error: any) {
// Only show restart message for connection errors, not HTTP errors
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
}
// Re-throw HTTP errors and other errors as-is
throw error;
handleWorkerError(error);
}
console.log(createHookResponse('UserPromptSubmit', true));
+12 -23
View File
@@ -10,6 +10,10 @@ import { stdin } from 'process';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { handleWorkerError } from '../shared/hook-error-handler.js';
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
export interface PostToolUseInput {
session_id: string;
@@ -17,18 +21,8 @@ export interface PostToolUseInput {
tool_name: string;
tool_input: any;
tool_response: any;
[key: string]: any;
}
// Tools to skip (low value or too frequent)
const SKIP_TOOLS = new Set([
'ListMcpResourcesTool', // MCP infrastructure
'SlashCommand', // Command invocation (observe what it produces, not the call)
'Skill', // Skill invocation (observe what it produces, not the call)
'TodoWrite', // Task management meta-tool
'AskUserQuestion' // User interaction, not substantive work
]);
/**
* Save Hook Main Logic - Fire-and-forget HTTP client
*/
@@ -42,11 +36,6 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
const { session_id, cwd, tool_name, tool_input, tool_response } = input;
if (SKIP_TOOLS.has(tool_name)) {
console.log(createHookResponse('PostToolUse', true));
return;
}
const port = getWorkerPort();
const toolStr = logger.formatTool(tool_name, tool_input);
@@ -65,9 +54,13 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
tool_name,
tool_input,
tool_response,
cwd: cwd || ''
cwd: happy_path_error__with_fallback(
'Missing cwd in PostToolUse hook input',
{ session_id, tool_name },
cwd || ''
)
}),
signal: AbortSignal.timeout(2000)
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (!response.ok) {
@@ -75,16 +68,12 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
logger.failure('HOOK', 'Failed to send observation', {
status: response.status
}, errorText);
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
}
logger.debug('HOOK', 'Observation sent successfully', { toolName: tool_name });
} catch (error: any) {
// Only show restart message for connection errors, not HTTP errors
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
}
throw error;
handleWorkerError(error);
}
console.log(createHookResponse('PostToolUse', true));
+30 -122
View File
@@ -10,120 +10,19 @@
*/
import { stdin } from 'process';
import { readFileSync, existsSync } from 'fs';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { handleWorkerError } from '../shared/hook-error-handler.js';
import { extractLastMessage } from '../shared/transcript-parser.js';
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
export interface StopInput {
session_id: string;
cwd: string;
transcript_path?: string;
[key: string]: any;
}
/**
* Extract last user message from transcript JSONL file
*/
function extractLastUserMessage(transcriptPath: string): string {
if (!transcriptPath || !existsSync(transcriptPath)) {
return '';
}
try {
const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) {
return '';
}
const lines = content.split('\n');
// Parse JSONL and find last user message
for (let i = lines.length - 1; i >= 0; i--) {
try {
const line = JSON.parse(lines[i]);
// Claude Code transcript format: {type: "user", message: {role: "user", content: [...]}}
if (line.type === 'user' && line.message?.content) {
const content = line.message.content;
// Extract text content (handle both string and array formats)
if (typeof content === 'string') {
return content;
} else if (Array.isArray(content)) {
const textParts = content
.filter((c: any) => c.type === 'text')
.map((c: any) => c.text);
return textParts.join('\n');
}
}
} catch (parseError) {
// Skip malformed lines
continue;
}
}
} catch (error) {
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
}
return '';
}
/**
* Extract last assistant message from transcript JSONL file
* Filters out system-reminder tags to avoid polluting summaries
*/
function extractLastAssistantMessage(transcriptPath: string): string {
if (!transcriptPath || !existsSync(transcriptPath)) {
return '';
}
try {
const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) {
return '';
}
const lines = content.split('\n');
// Parse JSONL and find last assistant message
for (let i = lines.length - 1; i >= 0; i--) {
try {
const line = JSON.parse(lines[i]);
// Claude Code transcript format: {type: "assistant", message: {role: "assistant", content: [...]}}
if (line.type === 'assistant' && line.message?.content) {
let text = '';
const content = line.message.content;
// Extract text content (handle both string and array formats)
if (typeof content === 'string') {
text = content;
} else if (Array.isArray(content)) {
const textParts = content
.filter((c: any) => c.type === 'text')
.map((c: any) => c.text);
text = textParts.join('\n');
}
// Filter out system-reminder tags and their content
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '');
// Clean up excessive whitespace
text = text.replace(/\n{3,}/g, '\n\n').trim();
return text;
}
} catch (parseError) {
// Skip malformed lines
continue;
}
}
} catch (error) {
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
}
return '';
transcript_path: string;
}
/**
@@ -142,8 +41,13 @@ async function summaryHook(input?: StopInput): Promise<void> {
const port = getWorkerPort();
// Extract last user AND assistant messages from transcript
const lastUserMessage = extractLastUserMessage(input.transcript_path || '');
const lastAssistantMessage = extractLastAssistantMessage(input.transcript_path || '');
const transcriptPath = happy_path_error__with_fallback(
'Missing transcript_path in Stop hook input',
{ session_id },
input.transcript_path || ''
);
const lastUserMessage = extractLastMessage(transcriptPath, 'user');
const lastAssistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
logger.dataIn('HOOK', 'Stop: Requesting summary', {
workerPort: port,
@@ -161,7 +65,7 @@ async function summaryHook(input?: StopInput): Promise<void> {
last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage
}),
signal: AbortSignal.timeout(2000)
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (!response.ok) {
@@ -169,23 +73,27 @@ async function summaryHook(input?: StopInput): Promise<void> {
logger.failure('HOOK', 'Failed to generate summary', {
status: response.status
}, errorText);
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
}
logger.debug('HOOK', 'Summary request sent successfully');
} catch (error: any) {
// Only show restart message for connection errors, not HTTP errors
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
}
throw error;
handleWorkerError(error);
} finally {
// Notify worker to stop spinner (fire-and-forget)
fetch(`http://127.0.0.1:${port}/api/processing`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isProcessing: false })
}).catch(() => {});
// Stop processing spinner
try {
const spinnerResponse = await fetch(`http://127.0.0.1:${port}/api/processing`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isProcessing: false }),
signal: AbortSignal.timeout(2000)
});
if (!spinnerResponse.ok) {
logger.warn('HOOK', 'Failed to stop spinner', { status: spinnerResponse.status });
}
} catch (error: any) {
logger.warn('HOOK', 'Could not stop spinner', { error: error.message });
}
}
console.log(createHookResponse('Stop', true));
+25 -77
View File
@@ -6,38 +6,10 @@
* has been loaded into their session. Uses stderr as the communication channel
* since it's currently the only way to display messages in Claude Code UI.
*/
import { join, basename } from "path";
import { homedir } from "os";
import { existsSync } from "fs";
import { basename } from "path";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
// Check if node_modules exists - if not, this is first run
const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const nodeModulesPath = join(pluginDir, 'node_modules');
if (!existsSync(nodeModulesPath)) {
// First-time installation - dependencies not yet installed
console.error(`
---
🎉 Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
user messages in Claude Code UI until a better method is provided.
---
Claude-Mem: First-Time Setup
Dependencies have been installed in the background. This only happens once.
💡 TIPS:
Memories will start generating while you work
Use /init to write or update your CLAUDE.md for better project context
Try /clear after one session to see what context looks like
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`);
process.exit(3);
}
import { HOOK_EXIT_CODES } from "../shared/hook-constants.js";
import { getWorkerRestartInstructions } from "../utils/error-messages.js";
try {
// Ensure worker is running
@@ -53,65 +25,41 @@ try {
);
if (!response.ok) {
throw new Error(`Worker error ${response.status}`);
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
}
const output = await response.text();
// If it's after Dec 5, 2025 7pm EST, patch this out
const now = new Date();
const amaEndDate = new Date('2025-12-06T00:00:00Z'); // Dec 5, 2025 7pm EST
// Product Hunt launch announcement - expires Dec 5, 2025 12am EST (05:00 UTC)
const phLaunchEndDate = new Date('2025-12-05T05:00:00Z');
let productHuntAnnouncement = "";
if (now < phLaunchEndDate) {
productHuntAnnouncement = `
🚀 🚀
We launched on Product Hunt!
https://tinyurl.com/claude-mem-ph
Your upvote means the world - thank you!
🚀 🚀
`;
}
let amaAnnouncement = "";
if (now < amaEndDate) {
// Check if we're during the live event (Dec 1-5, 5pm-7pm EST daily)
const estOffset = 5 * 60; // EST is UTC-5
const nowUtcMinutes = now.getUTCHours() * 60 + now.getUTCMinutes();
const estHour = Math.floor((nowUtcMinutes - estOffset + 1440) % 1440 / 60);
const day = now.getUTCDate();
const month = now.getUTCMonth();
const year = now.getUTCFullYear();
const isDec1to5 = year === 2025 && month === 11 && day >= 1 && day <= 5;
const isDuringLiveHours = estHour >= 17 && estHour < 19; // 5pm-7pm EST
if (isDec1to5 && isDuringLiveHours) {
amaAnnouncement = "\n 🔴 LIVE NOW: AMA w/ Dev (@thedotmack) until 7pm EST\n";
} else {
amaAnnouncement = "\n LIVE AMA w/ Dev (@thedotmack) Dec 1st5th, 5pm to 7pm EST\n";
}
}
console.error(
"\n\n📝 Claude-Mem Context Loaded\n" +
" ️ Note: This appears as stderr but is informational only\n\n" +
output +
"\n\n💡 New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.\n" +
"\n💬 Community https://discord.gg/J4wttp9vDu" +
productHuntAnnouncement +
amaAnnouncement +
`\n📺 Watch live in browser http://localhost:${port}/\n`
);
} catch (error) {
console.error(`❌ Failed to load context display: ${error}`);
// Context not available yet - likely first run or worker starting up
console.error(`
---
🎉 Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
user messages in Claude Code UI until a better method is provided.
---
Claude-Mem: First-Time Setup
Dependencies are installing in the background. This only happens once.
💡 TIPS:
Memories will start generating while you work
Use /init to write or update your CLAUDE.md for better project context
Try /clear after one session to see what context looks like
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`);
}
process.exit(3);
process.exit(HOOK_EXIT_CODES.USER_MESSAGE_ONLY);
+7 -1
View File
@@ -3,6 +3,8 @@
* Generates prompts for the Claude Agent SDK memory worker
*/
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
export interface Observation {
id: number;
tool_name: string;
@@ -175,7 +177,11 @@ export function buildObservationPrompt(obs: Observation): string {
* Build prompt to generate progress summary
*/
export function buildSummaryPrompt(session: SDKSession): string {
const lastAssistantMessage = session.last_assistant_message || '';
const lastAssistantMessage = happy_path_error__with_fallback(
'Missing last_assistant_message in session for summary prompt',
session,
session.last_assistant_message || ''
);
return `PROGRESS SUMMARY CHECKPOINT
===========================
+15 -13
View File
@@ -14,13 +14,15 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { silentDebug } from '../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
/**
* Worker HTTP API configuration
*/
const WORKER_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
const WORKER_BASE_URL = `http://localhost:${WORKER_PORT}`;
const WORKER_PORT = getWorkerPort();
const WORKER_HOST = getWorkerHost();
const WORKER_BASE_URL = `http://${WORKER_HOST}:${WORKER_PORT}`;
/**
* Map tool names to Worker HTTP endpoints
@@ -49,7 +51,7 @@ async function callWorkerAPI(
endpoint: string,
params: Record<string, any>
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
silentDebug('[search-server] → Worker API', { endpoint, params });
happy_path_error__with_fallback('[mcp-server] → Worker API', { endpoint, params });
try {
const searchParams = new URLSearchParams();
@@ -71,12 +73,12 @@ async function callWorkerAPI(
const data = await response.json() as { content: Array<{ type: 'text'; text: string }>; isError?: boolean };
silentDebug('[search-server] ← Worker API success', { endpoint });
happy_path_error__with_fallback('[mcp-server] ← Worker API success', { endpoint });
// Worker returns { content: [...] } format directly
return data;
} catch (error: any) {
silentDebug('[search-server] ← Worker API error', { endpoint, error: error.message });
happy_path_error__with_fallback('[mcp-server] ← Worker API error', { endpoint, error: error.message });
return {
content: [{
type: 'text' as const,
@@ -411,7 +413,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Cleanup function
async function cleanup() {
silentDebug('[search-server] Shutting down...');
happy_path_error__with_fallback('[mcp-server] Shutting down...');
process.exit(0);
}
@@ -424,22 +426,22 @@ async function main() {
// Start the MCP server
const transport = new StdioServerTransport();
await server.connect(transport);
silentDebug('[search-server] Claude-mem search server started');
happy_path_error__with_fallback('[mcp-server] Claude-mem search server started');
// Check Worker availability in background
setTimeout(async () => {
const workerAvailable = await verifyWorkerConnection();
if (!workerAvailable) {
silentDebug('[search-server] WARNING: Worker not available at', WORKER_BASE_URL);
silentDebug('[search-server] Tools will fail until Worker is started');
silentDebug('[search-server] Start Worker with: npm run worker:restart');
happy_path_error__with_fallback('[mcp-server] WARNING: Worker not available at', WORKER_BASE_URL);
happy_path_error__with_fallback('[mcp-server] Tools will fail until Worker is started');
happy_path_error__with_fallback('[mcp-server] Start Worker with: npm run worker:restart');
} else {
silentDebug('[search-server] Worker available at', WORKER_BASE_URL);
happy_path_error__with_fallback('[mcp-server] Worker available at', WORKER_BASE_URL);
}
}, 0);
}
main().catch((error) => {
silentDebug('[search-server] Fatal error:', error);
happy_path_error__with_fallback('[mcp-server] Fatal error:', error);
process.exit(1);
});
+2 -2
View File
@@ -16,7 +16,7 @@ import {
TYPE_WORK_EMOJI_MAP
} from '../constants/observation-metadata.js';
import { logger } from '../utils/logger.js';
import { SettingsDefaultsManager } from './worker/settings/SettingsDefaultsManager.js';
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
// 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');
@@ -335,7 +335,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
priorAssistantMessage = messages.assistantMessage;
}
} catch (error) {
// Ignore
// Expected: Transcript file may not exist or be readable
}
}
+260
View File
@@ -0,0 +1,260 @@
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
import { createWriteStream } from 'fs';
import { join } from 'path';
import { spawn } from 'child_process';
import { homedir } from 'os';
import { DATA_DIR } from '../../shared/paths.js';
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
const PID_FILE = join(DATA_DIR, 'worker.pid');
const LOG_DIR = join(DATA_DIR, 'logs');
const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Timeout constants
const PROCESS_STOP_TIMEOUT_MS = 5000;
const HEALTH_CHECK_TIMEOUT_MS = 10000;
const HEALTH_CHECK_INTERVAL_MS = 200;
const HEALTH_CHECK_FETCH_TIMEOUT_MS = 1000;
const PROCESS_EXIT_CHECK_INTERVAL_MS = 100;
interface PidInfo {
pid: number;
port: number;
startedAt: string;
version: string;
}
export class ProcessManager {
static async start(port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
// Validate port range
if (isNaN(port) || port < 1024 || port > 65535) {
return {
success: false,
error: `Invalid port ${port}. Must be between 1024 and 65535`
};
}
// Check if already running
if (await this.isRunning()) {
const info = this.getPidInfo();
return { success: true, pid: info?.pid };
}
// Ensure log directory exists
mkdirSync(LOG_DIR, { recursive: true });
// Get worker script path
const workerScript = join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs');
if (!existsSync(workerScript)) {
return { success: false, error: `Worker script not found at ${workerScript}` };
}
const logFile = this.getLogFilePath();
// Use Bun on all platforms
return this.startWithBun(workerScript, logFile, port);
}
private static isBunAvailable(): boolean {
return isBunAvailable();
}
private static async startWithBun(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
const bunPath = getBunPath();
if (!bunPath) {
return {
success: false,
error: 'Bun is required but not found in PATH or common installation paths. Install from https://bun.sh'
};
}
try {
const isWindows = process.platform === 'win32';
const child = spawn(bunPath, [script], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
cwd: MARKETPLACE_ROOT,
// Hide console window on Windows
...(isWindows && { windowsHide: true })
});
// Write logs
const logStream = createWriteStream(logFile, { flags: 'a' });
child.stdout?.pipe(logStream);
child.stderr?.pipe(logStream);
child.unref();
if (!child.pid) {
return { success: false, error: 'Failed to get PID from spawned process' };
}
// Write PID file
this.writePidFile({
pid: child.pid,
port,
startedAt: new Date().toISOString(),
version: process.env.npm_package_version || 'unknown'
});
// Wait for health
return this.waitForHealth(child.pid, port);
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise<boolean> {
const info = this.getPidInfo();
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 }> {
await this.stop();
return this.start(port);
}
static async status(): Promise<{ running: boolean; pid?: number; port?: number; uptime?: string }> {
const info = this.getPidInfo();
if (!info) return { running: false };
const running = this.isProcessAlive(info.pid);
return {
running,
pid: running ? info.pid : undefined,
port: running ? info.port : undefined,
uptime: running ? this.formatUptime(info.startedAt) : undefined
};
}
static async isRunning(): Promise<boolean> {
const info = this.getPidInfo();
if (!info) return false;
const alive = this.isProcessAlive(info.pid);
if (!alive) {
this.removePidFile(); // Clean up stale PID file
}
return alive;
}
// Helper methods
private static getPidInfo(): PidInfo | null {
try {
if (!existsSync(PID_FILE)) return null;
const content = readFileSync(PID_FILE, 'utf-8');
const parsed = JSON.parse(content);
// Validate required fields have correct types
if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number') {
return null;
}
return parsed as PidInfo;
} catch {
return null;
}
}
private static writePidFile(info: PidInfo): void {
mkdirSync(DATA_DIR, { recursive: true });
writeFileSync(PID_FILE, JSON.stringify(info, null, 2));
}
private static removePidFile(): void {
try {
if (existsSync(PID_FILE)) {
unlinkSync(PID_FILE);
}
} catch {
// Ignore errors
}
}
private static isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
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();
while (Date.now() - startTime < timeoutMs) {
// Check if process is still alive
if (!this.isProcessAlive(pid)) {
return { success: false, error: 'Process died during startup' };
}
// Try health check
try {
const response = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
});
if (response.ok) {
return { success: true, pid };
}
} catch {
// Not ready yet
}
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
}
return { success: false, error: 'Health check timed out' };
}
private static async waitForExit(pid: number, timeout: number): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (!this.isProcessAlive(pid)) {
return;
}
await new Promise(resolve => setTimeout(resolve, PROCESS_EXIT_CHECK_INTERVAL_MS));
}
throw new Error('Process did not exit within timeout');
}
private static getLogFilePath(): string {
const date = new Date().toISOString().slice(0, 10);
return join(LOG_DIR, `worker-${date}.log`);
}
private static formatUptime(startedAt: string): string {
const startTime = new Date(startedAt).getTime();
const now = Date.now();
const diffMs = now - startTime;
const seconds = Math.floor(diffMs / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${minutes % 60}m`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
}
}
+7 -3
View File
@@ -1,6 +1,10 @@
import { Database } from 'better-sqlite3';
import { Database } from 'bun:sqlite';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
// SQLite configuration constants
const SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024; // 256MB
const SQLITE_CACHE_SIZE_PAGES = 10_000;
export interface Migration {
version: number;
up: (db: Database) => void;
@@ -51,8 +55,8 @@ export class DatabaseManager {
this.db.run('PRAGMA synchronous = NORMAL');
this.db.run('PRAGMA foreign_keys = ON');
this.db.run('PRAGMA temp_store = memory');
this.db.run('PRAGMA mmap_size = 268435456'); // 256MB
this.db.run('PRAGMA cache_size = 10000');
this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
// Initialize schema_versions table
this.initializeSchemaVersions();
+9 -9
View File
@@ -1,4 +1,4 @@
import Database from 'better-sqlite3';
import { Database } from 'bun:sqlite';
import { TableNameRow } from '../../types/database.js';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
import {
@@ -18,7 +18,7 @@ import {
* Vector search is handled by ChromaDB - this class only supports filtering without query text
*/
export class SessionSearch {
private db: Database.Database;
private db: Database;
constructor(dbPath?: string) {
if (!dbPath) {
@@ -26,7 +26,7 @@ export class SessionSearch {
dbPath = DB_PATH;
}
this.db = new Database(dbPath);
this.db.pragma('journal_mode = WAL');
this.db.run('PRAGMA journal_mode = WAL');
// Ensure FTS tables exist
this.ensureFTSTables();
@@ -60,7 +60,7 @@ export class SessionSearch {
console.error('[SessionSearch] Creating FTS5 tables...');
// Create observations_fts virtual table
this.db.exec(`
this.db.run(`
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
title,
subtitle,
@@ -74,14 +74,14 @@ export class SessionSearch {
`);
// Populate with existing data
this.db.exec(`
this.db.run(`
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
SELECT id, title, subtitle, narrative, text, facts, concepts
FROM observations;
`);
// Create triggers for observations
this.db.exec(`
this.db.run(`
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
@@ -101,7 +101,7 @@ export class SessionSearch {
`);
// Create session_summaries_fts virtual table
this.db.exec(`
this.db.run(`
CREATE VIRTUAL TABLE IF NOT EXISTS session_summaries_fts USING fts5(
request,
investigated,
@@ -115,14 +115,14 @@ export class SessionSearch {
`);
// Populate with existing data
this.db.exec(`
this.db.run(`
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
SELECT id, request, investigated, learned, completed, next_steps, notes
FROM session_summaries;
`);
// Create triggers for session_summaries
this.db.exec(`
this.db.run(`
CREATE TRIGGER IF NOT EXISTS session_summaries_ai AFTER INSERT ON session_summaries BEGIN
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
+116 -58
View File
@@ -1,4 +1,4 @@
import Database from 'better-sqlite3';
import { Database } from 'bun:sqlite';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
import { logger } from '../../utils/logger.js';
import {
@@ -18,16 +18,16 @@ import {
* Provides simple, synchronous CRUD operations for session-based memory
*/
export class SessionStore {
public db: Database.Database;
public db: Database;
constructor() {
ensureDir(DATA_DIR);
this.db = new Database(DB_PATH);
// Ensure optimized settings
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.db.pragma('foreign_keys = ON');
this.db.run('PRAGMA journal_mode = WAL');
this.db.run('PRAGMA synchronous = NORMAL');
this.db.run('PRAGMA foreign_keys = ON');
// Initialize schema if needed (fresh database)
this.initializeSchema();
@@ -49,7 +49,7 @@ export class SessionStore {
private initializeSchema(): void {
try {
// Create schema_versions table if it doesn't exist
this.db.exec(`
this.db.run(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -67,7 +67,7 @@ export class SessionStore {
console.error('[SessionStore] Initializing fresh database with migration004...');
// Migration004: SDK agent architecture tables
this.db.exec(`
this.db.run(`
CREATE TABLE IF NOT EXISTS sdk_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL,
@@ -146,11 +146,11 @@ export class SessionStore {
if (applied) return;
// Check if column exists
const tableInfo = this.db.pragma('table_info(sdk_sessions)') as TableColumnInfo[];
const tableInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
const hasWorkerPort = tableInfo.some(col => col.name === 'worker_port');
if (!hasWorkerPort) {
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
this.db.run('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
console.error('[SessionStore] Added worker_port column to sdk_sessions table');
}
@@ -171,29 +171,29 @@ export class SessionStore {
if (applied) return;
// Check sdk_sessions for prompt_counter
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)') as TableColumnInfo[];
const sessionsInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
const hasPromptCounter = sessionsInfo.some(col => col.name === 'prompt_counter');
if (!hasPromptCounter) {
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0');
this.db.run('ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0');
console.error('[SessionStore] Added prompt_counter column to sdk_sessions table');
}
// Check observations for prompt_number
const observationsInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const observationsInfo = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
const obsHasPromptNumber = observationsInfo.some(col => col.name === 'prompt_number');
if (!obsHasPromptNumber) {
this.db.exec('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
this.db.run('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
console.error('[SessionStore] Added prompt_number column to observations table');
}
// Check session_summaries for prompt_number
const summariesInfo = this.db.pragma('table_info(session_summaries)') as TableColumnInfo[];
const summariesInfo = this.db.query('PRAGMA table_info(session_summaries)').all() as TableColumnInfo[];
const sumHasPromptNumber = summariesInfo.some(col => col.name === 'prompt_number');
if (!sumHasPromptNumber) {
this.db.exec('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
this.db.run('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
console.error('[SessionStore] Added prompt_number column to session_summaries table');
}
@@ -214,7 +214,7 @@ export class SessionStore {
if (applied) return;
// Check if UNIQUE constraint exists
const summariesIndexes = this.db.pragma('index_list(session_summaries)') as IndexInfo[];
const summariesIndexes = this.db.query('PRAGMA index_list(session_summaries)').all() as IndexInfo[];
const hasUniqueConstraint = summariesIndexes.some(idx => idx.unique === 1);
if (!hasUniqueConstraint) {
@@ -226,11 +226,11 @@ export class SessionStore {
console.error('[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id...');
// Begin transaction
this.db.exec('BEGIN TRANSACTION');
this.db.run('BEGIN TRANSACTION');
try {
// Create new table without UNIQUE constraint
this.db.exec(`
this.db.run(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
@@ -251,7 +251,7 @@ export class SessionStore {
`);
// Copy data from old table
this.db.exec(`
this.db.run(`
INSERT INTO session_summaries_new
SELECT id, sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
@@ -260,20 +260,20 @@ export class SessionStore {
`);
// Drop old table
this.db.exec('DROP TABLE session_summaries');
this.db.run('DROP TABLE session_summaries');
// Rename new table
this.db.exec('ALTER TABLE session_summaries_new RENAME TO session_summaries');
this.db.run('ALTER TABLE session_summaries_new RENAME TO session_summaries');
// Recreate indexes
this.db.exec(`
this.db.run(`
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`);
// Commit transaction
this.db.exec('COMMIT');
this.db.run('COMMIT');
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(7, new Date().toISOString());
@@ -281,7 +281,7 @@ export class SessionStore {
console.error('[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id');
} catch (error: any) {
// Rollback on error
this.db.exec('ROLLBACK');
this.db.run('ROLLBACK');
throw error;
}
} catch (error: any) {
@@ -299,7 +299,7 @@ export class SessionStore {
if (applied) return;
// Check if new fields already exist
const tableInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const tableInfo = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
const hasTitle = tableInfo.some(col => col.name === 'title');
if (hasTitle) {
@@ -311,7 +311,7 @@ export class SessionStore {
console.error('[SessionStore] Adding hierarchical fields to observations table...');
// Add new columns
this.db.exec(`
this.db.run(`
ALTER TABLE observations ADD COLUMN title TEXT;
ALTER TABLE observations ADD COLUMN subtitle TEXT;
ALTER TABLE observations ADD COLUMN facts TEXT;
@@ -341,7 +341,7 @@ export class SessionStore {
if (applied) return;
// Check if text column is already nullable
const tableInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const tableInfo = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
const textColumn = tableInfo.find(col => col.name === 'text');
if (!textColumn || textColumn.notnull === 0) {
@@ -353,11 +353,11 @@ export class SessionStore {
console.error('[SessionStore] Making observations.text nullable...');
// Begin transaction
this.db.exec('BEGIN TRANSACTION');
this.db.run('BEGIN TRANSACTION');
try {
// Create new table with text as nullable
this.db.exec(`
this.db.run(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
@@ -379,7 +379,7 @@ export class SessionStore {
`);
// Copy data from old table (all existing columns)
this.db.exec(`
this.db.run(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
@@ -388,13 +388,13 @@ export class SessionStore {
`);
// Drop old table
this.db.exec('DROP TABLE observations');
this.db.run('DROP TABLE observations');
// Rename new table
this.db.exec('ALTER TABLE observations_new RENAME TO observations');
this.db.run('ALTER TABLE observations_new RENAME TO observations');
// Recreate indexes
this.db.exec(`
this.db.run(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
@@ -402,7 +402,7 @@ export class SessionStore {
`);
// Commit transaction
this.db.exec('COMMIT');
this.db.run('COMMIT');
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(9, new Date().toISOString());
@@ -410,7 +410,7 @@ export class SessionStore {
console.error('[SessionStore] Successfully made observations.text nullable');
} catch (error: any) {
// Rollback on error
this.db.exec('ROLLBACK');
this.db.run('ROLLBACK');
throw error;
}
} catch (error: any) {
@@ -428,7 +428,7 @@ export class SessionStore {
if (applied) return;
// Check if table already exists
const tableInfo = this.db.pragma('table_info(user_prompts)') as TableColumnInfo[];
const tableInfo = this.db.query('PRAGMA table_info(user_prompts)').all() as TableColumnInfo[];
if (tableInfo.length > 0) {
// Already migrated
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(10, new Date().toISOString());
@@ -438,11 +438,11 @@ export class SessionStore {
console.error('[SessionStore] Creating user_prompts table with FTS5 support...');
// Begin transaction
this.db.exec('BEGIN TRANSACTION');
this.db.run('BEGIN TRANSACTION');
try {
// Create main table (using claude_session_id since sdk_session_id is set asynchronously by worker)
this.db.exec(`
this.db.run(`
CREATE TABLE user_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT NOT NULL,
@@ -460,7 +460,7 @@ export class SessionStore {
`);
// Create FTS5 virtual table
this.db.exec(`
this.db.run(`
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
prompt_text,
content='user_prompts',
@@ -469,7 +469,7 @@ export class SessionStore {
`);
// Create triggers to sync FTS5
this.db.exec(`
this.db.run(`
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
@@ -489,7 +489,7 @@ export class SessionStore {
`);
// Commit transaction
this.db.exec('COMMIT');
this.db.run('COMMIT');
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(10, new Date().toISOString());
@@ -497,7 +497,7 @@ export class SessionStore {
console.error('[SessionStore] Successfully created user_prompts table with FTS5 support');
} catch (error: any) {
// Rollback on error
this.db.exec('ROLLBACK');
this.db.run('ROLLBACK');
throw error;
}
} catch (error: any) {
@@ -517,20 +517,20 @@ export class SessionStore {
if (applied) return;
// Check if discovery_tokens column exists in observations table
const observationsInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const observationsInfo = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
const obsHasDiscoveryTokens = observationsInfo.some(col => col.name === 'discovery_tokens');
if (!obsHasDiscoveryTokens) {
this.db.exec('ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
this.db.run('ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
console.error('[SessionStore] Added discovery_tokens column to observations table');
}
// Check if discovery_tokens column exists in session_summaries table
const summariesInfo = this.db.pragma('table_info(session_summaries)') as TableColumnInfo[];
const summariesInfo = this.db.query('PRAGMA table_info(session_summaries)').all() as TableColumnInfo[];
const sumHasDiscoveryTokens = summariesInfo.some(col => col.name === 'discovery_tokens');
if (!sumHasDiscoveryTokens) {
this.db.exec('ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
this.db.run('ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
console.error('[SessionStore] Added discovery_tokens column to session_summaries table');
}
@@ -811,26 +811,72 @@ export class SessionStore {
*/
getObservationsByIds(
ids: number[],
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string; type?: string | string[]; concepts?: string | string[]; files?: string | string[] } = {}
): ObservationRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit } = options;
const { orderBy = 'date_desc', limit, project, type, concepts, files } = options;
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
const limitClause = limit ? `LIMIT ${limit}` : '';
// Build placeholders for IN clause
const placeholders = ids.map(() => '?').join(',');
const params: any[] = [...ids];
const additionalConditions: string[] = [];
// Apply project filter
if (project) {
additionalConditions.push('project = ?');
params.push(project);
}
// Apply type filter
if (type) {
if (Array.isArray(type)) {
const typePlaceholders = type.map(() => '?').join(',');
additionalConditions.push(`type IN (${typePlaceholders})`);
params.push(...type);
} else {
additionalConditions.push('type = ?');
params.push(type);
}
}
// Apply concepts filter
if (concepts) {
const conceptsList = Array.isArray(concepts) ? concepts : [concepts];
const conceptConditions = conceptsList.map(() =>
'EXISTS (SELECT 1 FROM json_each(concepts) WHERE value = ?)'
);
params.push(...conceptsList);
additionalConditions.push(`(${conceptConditions.join(' OR ')})`);
}
// Apply files filter
if (files) {
const filesList = Array.isArray(files) ? files : [files];
const fileConditions = filesList.map(() => {
return '(EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?) OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?))';
});
filesList.forEach(file => {
params.push(`%${file}%`, `%${file}%`);
});
additionalConditions.push(`(${fileConditions.join(' OR ')})`);
}
const whereClause = additionalConditions.length > 0
? `WHERE id IN (${placeholders}) AND ${additionalConditions.join(' AND ')}`
: `WHERE id IN (${placeholders})`;
const stmt = this.db.prepare(`
SELECT *
FROM observations
WHERE id IN (${placeholders})
${whereClause}
ORDER BY created_at_epoch ${orderClause}
${limitClause}
`);
return stmt.all(...ids) as ObservationRecord[];
return stmt.all(...params) as ObservationRecord[];
}
/**
@@ -1353,23 +1399,30 @@ export class SessionStore {
*/
getSessionSummariesByIds(
ids: number[],
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string } = {}
): SessionSummaryRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit } = options;
const { orderBy = 'date_desc', limit, project } = options;
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
const limitClause = limit ? `LIMIT ${limit}` : '';
const placeholders = ids.map(() => '?').join(',');
const params: any[] = [...ids];
// Apply project filter
const whereClause = project
? `WHERE id IN (${placeholders}) AND project = ?`
: `WHERE id IN (${placeholders})`;
if (project) params.push(project);
const stmt = this.db.prepare(`
SELECT * FROM session_summaries
WHERE id IN (${placeholders})
${whereClause}
ORDER BY created_at_epoch ${orderClause}
${limitClause}
`);
return stmt.all(...ids) as SessionSummaryRecord[];
return stmt.all(...params) as SessionSummaryRecord[];
}
/**
@@ -1378,14 +1431,19 @@ export class SessionStore {
*/
getUserPromptsByIds(
ids: number[],
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string } = {}
): UserPromptRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit } = options;
const { orderBy = 'date_desc', limit, project } = options;
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
const limitClause = limit ? `LIMIT ${limit}` : '';
const placeholders = ids.map(() => '?').join(',');
const params: any[] = [...ids];
// Apply project filter
const projectFilter = project ? 'AND s.project = ?' : '';
if (project) params.push(project);
const stmt = this.db.prepare(`
SELECT
@@ -1394,12 +1452,12 @@ export class SessionStore {
s.sdk_session_id
FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.id IN (${placeholders})
WHERE up.id IN (${placeholders}) ${projectFilter}
ORDER BY up.created_at_epoch ${orderClause}
${limitClause}
`);
return stmt.all(...ids) as UserPromptRecord[];
return stmt.all(...params) as UserPromptRecord[];
}
/**
+1 -1
View File
@@ -1,4 +1,4 @@
import { Database } from 'better-sqlite3';
import { Database } from 'bun:sqlite';
import { Migration } from './Database.js';
/**
+41 -8
View File
@@ -13,6 +13,9 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { ParsedObservation, ParsedSummary } from '../../sdk/parser.js';
import { SessionStore } from '../sqlite/SessionStore.js';
import { logger } from '../../utils/logger.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import path from 'path';
import os from 'os';
@@ -70,6 +73,7 @@ interface StoredUserPrompt {
export class ChromaSync {
private client: Client | null = null;
private transport: StdioClientTransport | null = null;
private connected: boolean = false;
private project: string;
private collectionName: string;
@@ -96,8 +100,9 @@ export class ChromaSync {
try {
// Use Python 3.13 by default to avoid onnxruntime compatibility issues with Python 3.14+
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
const pythonVersion = process.env.CLAUDE_MEM_PYTHON_VERSION || '3.13';
const transport = new StdioClientTransport({
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
this.transport = new StdioClientTransport({
command: 'uvx',
args: [
'--python', pythonVersion,
@@ -115,7 +120,7 @@ export class ChromaSync {
capabilities: {}
});
await this.client.connect(transport);
await this.client.connect(this.transport);
this.connected = true;
logger.info('CHROMA_SYNC', 'Connected to Chroma MCP server', { project: this.project });
@@ -763,7 +768,11 @@ export class ChromaSync {
arguments: arguments_obj
});
const resultText = result.content[0]?.text || '';
const resultText = happy_path_error__with_fallback(
'Missing text in MCP chroma_query_documents result',
{ project: this.project, query_text: query },
result.content[0]?.text || ''
);
// Parse JSON response
let parsed: any;
@@ -807,14 +816,38 @@ export class ChromaSync {
}
/**
* Close the Chroma client connection
* Close the Chroma client connection and cleanup subprocess
*/
async close(): Promise<void> {
if (this.client && this.connected) {
await this.client.close();
if (!this.connected && !this.client && !this.transport) {
return;
}
try {
// Close client first
if (this.client) {
try {
await this.client.close();
} catch (error) {
logger.warn('CHROMA_SYNC', 'Error closing Chroma client', { project: this.project }, error as Error);
}
}
// Explicitly close transport to kill subprocess
if (this.transport) {
try {
await this.transport.close();
} catch (error) {
logger.warn('CHROMA_SYNC', 'Error closing transport', { project: this.project }, error as Error);
}
}
logger.info('CHROMA_SYNC', 'Chroma client and subprocess closed', { project: this.project });
} finally {
// Always reset state, even if errors occurred
this.connected = false;
this.client = null;
logger.info('CHROMA_SYNC', 'Chroma client closed', { project: this.project });
this.transport = null;
}
}
}
+91 -4
View File
@@ -11,8 +11,12 @@ import http from 'http';
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { getWorkerPort } from '../shared/worker-utils.js';
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
import { logger } from '../utils/logger.js';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
// Import composed domain services
import { DatabaseManager } from './worker/DatabaseManager.js';
@@ -108,6 +112,39 @@ export class WorkerService {
res.status(200).json({ status: 'ok' });
});
// Version endpoint - returns the worker's current version
this.app.get('/api/version', (_req, res) => {
try {
// 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'));
res.status(200).json({ version: packageJson.version });
} catch (error) {
logger.error('SYSTEM', 'Failed to read version', {}, error as Error);
res.status(500).json({ error: 'Failed to read version' });
}
});
// Admin endpoints for process management
this.app.post('/api/admin/restart', async (_req, res) => {
res.json({ status: 'restarting' });
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
});
this.app.post('/api/admin/shutdown', async (_req, res) => {
res.json({ status: 'shutting_down' });
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
});
this.viewerRoutes.setupRoutes(this.app);
this.sessionRoutes.setupRoutes(this.app);
this.dataRoutes.setupRoutes(this.app);
@@ -116,18 +153,65 @@ export class WorkerService {
}
/**
* Clean up orphaned chroma-mcp processes from previous worker sessions
* Prevents process accumulation and memory leaks
*/
private async cleanupOrphanedProcesses(): Promise<void> {
try {
// Find all chroma-mcp processes
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[] = [];
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length > 1) {
const pid = parseInt(parts[1], 10);
if (!isNaN(pid)) {
pids.push(pid);
}
}
}
if (pids.length === 0) {
return;
}
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
count: pids.length,
pids
});
// Kill all found processes
await execAsync(`kill ${pids.join(' ')}`);
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
} catch (error) {
// Non-fatal - log and continue
logger.warn('SYSTEM', 'Failed to cleanup orphaned processes', {}, error as Error);
}
}
/**
* Start the worker service
*/
async start(): Promise<void> {
// Start HTTP server FIRST - make port available immediately
const port = getWorkerPort();
const host = getWorkerHost();
this.server = await new Promise<http.Server>((resolve, reject) => {
const srv = this.app.listen(port, () => resolve(srv));
const srv = this.app.listen(port, host, () => resolve(srv));
srv.on('error', reject);
});
logger.info('SYSTEM', 'Worker started', { port, pid: process.pid });
logger.info('SYSTEM', 'Worker started', { host, port, pid: process.pid });
// Do slow initialization in background (non-blocking)
this.initializeBackground().catch((error) => {
@@ -139,6 +223,9 @@ export class WorkerService {
* Background initialization - runs after HTTP server is listening
*/
private async initializeBackground(): Promise<void> {
// Clean up any orphaned chroma-mcp processes BEFORE starting our own
await this.cleanupOrphanedProcesses();
// Initialize database (once, stays open)
await this.dbManager.initialize();
@@ -157,7 +244,7 @@ export class WorkerService {
logger.info('WORKER', 'SearchManager initialized and search routes registered');
// Connect to MCP server
const mcpServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs');
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
const transport = new StdioClientTransport({
command: 'node',
args: [mcpServerPath],
+12 -5
View File
@@ -13,6 +13,11 @@ import { logger } from '../../utils/logger.js';
const INSTALLED_PLUGIN_PATH = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Timeout constants
const GIT_COMMAND_TIMEOUT_MS = 30_000;
const NPM_INSTALL_TIMEOUT_MS = 120_000;
const DEFAULT_SHELL_TIMEOUT_MS = 60_000;
export interface BranchInfo {
branch: string | null;
isBeta: boolean;
@@ -36,18 +41,20 @@ function execGit(command: string): string {
return execSync(`git ${command}`, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: 30000
timeout: GIT_COMMAND_TIMEOUT_MS,
windowsHide: true
}).trim();
}
/**
* Execute shell command in installed plugin directory
*/
function execShell(command: string, timeoutMs: number = 60000): string {
function execShell(command: string, timeoutMs: number = DEFAULT_SHELL_TIMEOUT_MS): string {
return execSync(command, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: timeoutMs
timeout: timeoutMs,
windowsHide: true
}).trim();
}
@@ -163,7 +170,7 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
}
logger.debug('BRANCH', 'Running npm install');
execShell('npm install', 120000); // 2 minute timeout for npm
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Branch switch complete', {
branch: targetBranch
@@ -221,7 +228,7 @@ export async function pullUpdates(): Promise<SwitchResult> {
if (existsSync(installMarker)) {
unlinkSync(installMarker);
}
execShell('npm install', 120000);
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Updates pulled', { branch: info.branch });
+19 -6
View File
@@ -4,6 +4,7 @@
*/
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
import { logger } from '../../utils/logger.js';
export type FormatType = 'index' | 'full';
@@ -102,7 +103,9 @@ Other tips:
if (facts.length > 0) {
metadata.push(`Facts: ${facts.join('; ')}`);
}
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in facts field', { obsId: obs.id });
}
}
if (obs.concepts) {
@@ -111,7 +114,9 @@ Other tips:
if (concepts.length > 0) {
metadata.push(`Concepts: ${concepts.join(', ')}`);
}
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in concepts field', { obsId: obs.id });
}
}
if (obs.files_read || obs.files_modified) {
@@ -119,12 +124,16 @@ Other tips:
if (obs.files_read) {
try {
files.push(...JSON.parse(obs.files_read));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in files_read field', { obsId: obs.id });
}
}
if (obs.files_modified) {
try {
files.push(...JSON.parse(obs.files_modified));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in files_modified field', { obsId: obs.id });
}
}
if (files.length > 0) {
metadata.push(`Files: ${[...new Set(files)].join(', ')}`);
@@ -190,12 +199,16 @@ Other tips:
if (session.files_read) {
try {
files.push(...JSON.parse(session.files_read));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in session files_read field', { sessionId: session.sdk_session_id });
}
}
if (session.files_edited) {
try {
files.push(...JSON.parse(session.files_edited));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in session files_edited field', { sessionId: session.sdk_session_id });
}
}
if (files.length > 0) {
metadata.push(`Files: ${[...new Set(files)].join(', ')}`);
+22 -12
View File
@@ -14,10 +14,11 @@ import path from 'path';
import { DatabaseManager } from './DatabaseManager.js';
import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { silentDebug } from '../../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import { parseObservations, parseSummary } from '../../sdk/parser.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from './settings/SettingsDefaultsManager.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
// Import Agent SDK (assumes it's installed)
@@ -232,8 +233,16 @@ export class SDKAgent {
sdk_session_id: session.sdkSessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
last_user_message: happy_path_error__with_fallback(
'Missing last_user_message for summary in SDKAgent',
{ sessionDbId: session.sessionDbId, sdkSessionId: session.sdkSessionId },
message.last_user_message || ''
),
last_assistant_message: happy_path_error__with_fallback(
'Missing last_assistant_message for summary in SDKAgent',
{ sessionDbId: session.sessionDbId, sdkSessionId: session.sdkSessionId },
message.last_assistant_message || ''
)
})
},
session_id: session.claudeSessionId,
@@ -267,16 +276,16 @@ export class SDKAgent {
sessionId: session.sessionDbId,
obsId,
type: obs.type,
title: obs.title || silentDebug('obs.title is null', { obsId, type: obs.type }, '(untitled)'),
filesRead: obs.files_read?.length ?? (silentDebug('obs.files_read is null/undefined', { obsId }), 0),
filesModified: obs.files_modified?.length ?? (silentDebug('obs.files_modified is null/undefined', { obsId }), 0),
concepts: obs.concepts?.length ?? (silentDebug('obs.concepts is null/undefined', { obsId }), 0)
title: obs.title || happy_path_error__with_fallback('obs.title is null', { obsId, type: obs.type }, '(untitled)'),
filesRead: obs.files_read?.length ?? (happy_path_error__with_fallback('obs.files_read is null/undefined', { obsId }), 0),
filesModified: obs.files_modified?.length ?? (happy_path_error__with_fallback('obs.files_modified is null/undefined', { obsId }), 0),
concepts: obs.concepts?.length ?? (happy_path_error__with_fallback('obs.concepts is null/undefined', { obsId }), 0)
});
// Sync to Chroma with error logging
const chromaStart = Date.now();
const obsType = obs.type;
const obsTitle = obs.title || silentDebug('obs.title is null for Chroma sync', { obsId, type: obs.type }, '(untitled)');
const obsTitle = obs.title || happy_path_error__with_fallback('obs.title is null for Chroma sync', { obsId, type: obs.type }, '(untitled)');
this.dbManager.getChromaSync().syncObservation(
obsId,
session.claudeSessionId,
@@ -344,14 +353,14 @@ export class SDKAgent {
logger.info('SDK', 'Summary saved', {
sessionId: session.sessionDbId,
summaryId,
request: summary.request || silentDebug('summary.request is null', { summaryId }, '(no request)'),
request: summary.request || happy_path_error__with_fallback('summary.request is null', { summaryId }, '(no request)'),
hasCompleted: !!summary.completed,
hasNextSteps: !!summary.next_steps
});
// Sync to Chroma with error logging
const chromaStart = Date.now();
const summaryRequest = summary.request || silentDebug('summary.request is null for Chroma sync', { summaryId }, '(no request)');
const summaryRequest = summary.request || happy_path_error__with_fallback('summary.request is null for Chroma sync', { summaryId }, '(no request)');
this.dbManager.getChromaSync().syncSummary(
summaryId,
session.claudeSessionId,
@@ -410,7 +419,8 @@ export class SDKAgent {
* Find Claude executable (inline, called once per session)
*/
private findClaudeExecutable(): string {
const claudePath = process.env.CLAUDE_CODE_PATH ||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
const claudePath = settings.CLAUDE_CODE_PATH ||
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8', windowsHide: true })
.trim().split('\n')[0].trim();
+77 -61
View File
@@ -13,7 +13,7 @@ import { ChromaSync } from '../sync/ChromaSync.js';
import { FormattingService } from './FormattingService.js';
import { TimelineService, TimelineItem } from './TimelineService.js';
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
import { silentDebug } from '../../utils/silent-debug.js';
import { logger } from '../../utils/logger.js';
const COLLECTION_NAME = 'cm__claude-mem';
@@ -97,7 +97,7 @@ export class SearchManager {
// PATH 1: FILTER-ONLY (no query text) - Skip Chroma/FTS5, use direct SQLite filtering
// This path enables date filtering which Chroma cannot do (requires direct SQLite access)
if (!query) {
silentDebug(`[search-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
logger.debug('SEARCH', 'Filter-only query (no query text), using direct SQLite filtering', { enablesDateFilters: true });
const obsOptions = { ...options, type: obs_type, concepts, files };
if (searchObservations) {
observations = this.sessionSearch.searchObservations(undefined, obsOptions);
@@ -113,7 +113,7 @@ export class SearchManager {
else if (this.chromaSync) {
let chromaSucceeded = false;
try {
silentDebug(`[search-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
logger.debug('SEARCH', 'Using ChromaDB semantic search', { typeFilter: type || 'all' });
// Build Chroma where filter for doc_type
let whereFilter: Record<string, any> | undefined;
@@ -128,7 +128,7 @@ export class SearchManager {
// Step 1: Chroma semantic search with optional type filter
const chromaResults = await this.queryChroma(query, 100, whereFilter);
chromaSucceeded = true; // Chroma didn't throw error
silentDebug(`[search-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'ChromaDB returned semantic matches', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -139,7 +139,7 @@ export class SearchManager {
isRecent: meta && meta.created_at_epoch > ninetyDaysAgo
})).filter(item => item.isRecent);
silentDebug(`[search-server] ${recentMetadata.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentMetadata.length });
// Step 3: Categorize IDs by document type
const obsIds: number[] = [];
@@ -157,7 +157,7 @@ export class SearchManager {
}
}
silentDebug(`[search-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
logger.debug('SEARCH', 'Categorized results by type', { observations: obsIds.length, sessions: sessionIds.length, prompts: promptIds.length });
// Step 4: Hydrate from SQLite with additional filters
if (obsIds.length > 0) {
@@ -166,20 +166,20 @@ export class SearchManager {
observations = this.sessionStore.getObservationsByIds(obsIds, obsOptions);
}
if (sessionIds.length > 0) {
sessions = this.sessionStore.getSessionSummariesByIds(sessionIds, { orderBy: 'date_desc', limit: options.limit });
sessions = this.sessionStore.getSessionSummariesByIds(sessionIds, { orderBy: 'date_desc', limit: options.limit, project: options.project });
}
if (promptIds.length > 0) {
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit });
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit, project: options.project });
}
silentDebug(`[search-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
logger.debug('SEARCH', 'Hydrated results from SQLite', { observations: observations.length, sessions: sessions.length, prompts: prompts.length });
} else {
// Chroma returned 0 results - this is the correct answer, don't fall back to FTS5
silentDebug(`[search-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
logger.debug('SEARCH', 'ChromaDB found no matches (final result, no FTS5 fallback)', {});
}
} catch (chromaError: any) {
silentDebug('[search-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
logger.debug('SEARCH', 'ChromaDB failed - returning empty results (FTS5 fallback removed)', { error: chromaError.message });
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
// Return empty results - no fallback
observations = [];
sessions = [];
@@ -188,8 +188,8 @@ export class SearchManager {
}
// ChromaDB not initialized - return empty results (no fallback)
else {
silentDebug(`[search-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`);
silentDebug(`[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
logger.debug('SEARCH', 'ChromaDB not initialized - returning empty results (FTS5 fallback removed)', {});
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
observations = [];
sessions = [];
prompts = [];
@@ -198,6 +198,13 @@ export class SearchManager {
const totalResults = observations.length + sessions.length + prompts.length;
if (totalResults === 0) {
if (format === 'json') {
return {
observations: [],
sessions: [],
prompts: []
};
}
return {
content: [{
type: 'text' as const,
@@ -230,6 +237,15 @@ export class SearchManager {
const limitedResults = allResults.slice(0, options.limit || 20);
// Format based on requested format
if (format === 'json') {
// Raw JSON format for exports
return {
observations,
sessions,
prompts
};
}
let combinedText: string;
if (format === 'index') {
const header = `Found ${totalResults} result(s) matching "${query}" (${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts):\n\n`;
@@ -312,9 +328,9 @@ export class SearchManager {
if (this.chromaSync) {
try {
silentDebug('[search-server] Using hybrid semantic search for timeline query');
logger.debug('SEARCH', 'Using hybrid semantic search for timeline query', {});
const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[search-server] Chroma returned ${chromaResults?.ids?.length ?? 0} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for timeline', { matchCount: chromaResults?.ids?.length ?? 0 });
if (chromaResults?.ids && chromaResults.ids.length > 0) {
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
@@ -328,7 +344,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -345,7 +361,7 @@ export class SearchManager {
const topResult = results[0];
anchorId = topResult.id;
anchorEpoch = topResult.created_at_epoch;
silentDebug(`[search-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
logger.debug('SEARCH', 'Query mode: Using observation as timeline anchor', { observationId: topResult.id });
timelineData = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project);
}
// MODE 2: Anchor-based timeline
@@ -621,7 +637,7 @@ export class SearchManager {
try {
if (query) {
// Semantic search filtered to decision type
silentDebug('[search-server] Using Chroma semantic search with type=decision filter');
logger.debug('SEARCH', 'Using Chroma semantic search with type=decision filter', {});
const chromaResults = await this.queryChroma(query, Math.min((filters.limit || 20) * 2, 100), { type: 'decision' });
const obsIds = chromaResults.ids;
@@ -632,7 +648,7 @@ export class SearchManager {
}
} else {
// No query: get all decisions, rank by "decision" keyword
silentDebug('[search-server] Using metadata-first + semantic ranking for decisions');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for decisions', {});
const metadataResults = this.sessionSearch.findByType('decision', filters);
if (metadataResults.length > 0) {
@@ -653,7 +669,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma search failed, using SQLite fallback:', chromaError.message);
logger.debug('SEARCH', 'Chroma search failed, using SQLite fallback', { error: chromaError.message });
}
}
@@ -709,7 +725,7 @@ export class SearchManager {
// Search for change-type observations and change-related concepts
if (this.chromaSync) {
try {
silentDebug('[search-server] Using hybrid search for change-related observations');
logger.debug('SEARCH', 'Using hybrid search for change-related observations', {});
// Get all observations with type="change" or concepts containing change
const typeResults = this.sessionSearch.findByType('change', filters);
@@ -737,7 +753,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
}
}
@@ -807,7 +823,7 @@ export class SearchManager {
// Search for how-it-works concept observations
if (this.chromaSync) {
try {
silentDebug('[search-server] Using metadata-first + semantic ranking for how-it-works');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for how-it-works', {});
const metadataResults = this.sessionSearch.findByConcept('how-it-works', filters);
if (metadataResults.length > 0) {
@@ -827,7 +843,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
}
}
@@ -883,11 +899,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
silentDebug('[search-server] Using hybrid semantic search (Chroma + SQLite)');
logger.debug('SEARCH', 'Using hybrid semantic search (Chroma + SQLite)', {});
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -897,17 +913,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[search-server] Hydrated ${results.length} observations from SQLite`);
logger.debug('SEARCH', 'Hydrated observations from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -960,11 +976,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
silentDebug('[search-server] Using hybrid semantic search for sessions');
logger.debug('SEARCH', 'Using hybrid semantic search for sessions', {});
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'session_summary' });
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for sessions', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -974,17 +990,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[search-server] Hydrated ${results.length} sessions from SQLite`);
logger.debug('SEARCH', 'Hydrated sessions from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -1037,11 +1053,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
silentDebug('[search-server] Using hybrid semantic search for user prompts');
logger.debug('SEARCH', 'Using hybrid semantic search for user prompts', {});
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'user_prompt' });
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for prompts', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -1051,17 +1067,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[search-server] Hydrated ${results.length} user prompts from SQLite`);
logger.debug('SEARCH', 'Hydrated user prompts from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -1114,11 +1130,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search
if (this.chromaSync) {
try {
silentDebug('[search-server] Using metadata-first + semantic ranking for concept search');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for concept search', {});
// Step 1: SQLite metadata filter (get all IDs with this concept)
const metadataResults = this.sessionSearch.findByConcept(concept, filters);
silentDebug(`[search-server] Found ${metadataResults.length} observations with concept "${concept}"`);
logger.debug('SEARCH', 'Found observations with concept', { concept, count: metadataResults.length });
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to concept)
@@ -1133,7 +1149,7 @@ export class SearchManager {
}
}
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
logger.debug('SEARCH', 'Chroma ranked results by semantic relevance', { count: rankedIds.length });
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1143,14 +1159,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
silentDebug('[search-server] Using SQLite-only concept search');
logger.debug('SEARCH', 'Using SQLite-only concept search', {});
results = this.sessionSearch.findByConcept(concept, filters);
}
@@ -1204,11 +1220,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search for observations
if (this.chromaSync) {
try {
silentDebug('[search-server] Using metadata-first + semantic ranking for file search');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for file search', {});
// Step 1: SQLite metadata filter (get all results with this file)
const metadataResults = this.sessionSearch.findByFile(filePath, filters);
silentDebug(`[search-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`);
logger.debug('SEARCH', 'Found results for file', { file: filePath, observations: metadataResults.observations.length, sessions: metadataResults.sessions.length });
// Sessions: Keep as-is (already summarized, no semantic ranking needed)
sessions = metadataResults.sessions;
@@ -1227,7 +1243,7 @@ export class SearchManager {
}
}
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
logger.debug('SEARCH', 'Chroma ranked observations by semantic relevance', { count: rankedIds.length });
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1237,14 +1253,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (observations.length === 0 && sessions.length === 0) {
silentDebug('[search-server] Using SQLite-only file search');
logger.debug('SEARCH', 'Using SQLite-only file search', {});
const results = this.sessionSearch.findByFile(filePath, filters);
observations = results.observations;
sessions = results.sessions;
@@ -1323,11 +1339,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search
if (this.chromaSync) {
try {
silentDebug('[search-server] Using metadata-first + semantic ranking for type search');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for type search', {});
// Step 1: SQLite metadata filter (get all IDs with this type)
const metadataResults = this.sessionSearch.findByType(type, filters);
silentDebug(`[search-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
logger.debug('SEARCH', 'Found observations with type', { type: typeStr, count: metadataResults.length });
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to type)
@@ -1342,7 +1358,7 @@ export class SearchManager {
}
}
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
logger.debug('SEARCH', 'Chroma ranked results by semantic relevance', { count: rankedIds.length });
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1352,14 +1368,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
silentDebug('[search-server] Using SQLite-only type search');
logger.debug('SEARCH', 'Using SQLite-only type search', {});
results = this.sessionSearch.findByType(type, filters);
}
@@ -1815,9 +1831,9 @@ export class SearchManager {
// Use hybrid search if available
if (this.chromaSync) {
try {
silentDebug('[search-server] Using hybrid semantic search for timeline query');
logger.debug('SEARCH', 'Using hybrid semantic search for timeline query', {});
const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for timeline', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Filter by recency (90 days)
@@ -1827,15 +1843,15 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
if (recentIds.length > 0) {
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit: mode === 'auto' ? 1 : limit });
silentDebug(`[search-server] Hydrated ${results.length} observations from SQLite`);
logger.debug('SEARCH', 'Hydrated observations from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -1886,7 +1902,7 @@ export class SearchManager {
} else {
// Auto mode: Use top result as timeline anchor
const topResult = results[0];
silentDebug(`[search-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
logger.debug('SEARCH', 'Auto mode: Using observation as timeline anchor', { observationId: topResult.id });
// Get timeline around this observation
const timelineData = this.sessionStore.getTimelineAroundObservation(

Some files were not shown because too many files have changed in this diff Show More