Compare commits

...

15 Commits

Author SHA1 Message Date
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
Alex Newman 5210bc74c7 chore: bump version to 7.0.1
Fix: Ensure worker is running at the beginning of all hook files

- Move ensureWorkerRunning to the start of all hook functions
- Replace waitForPort with ensureWorkerRunning in context-hook
- Ensures worker is started before any other hook logic executes
- Improves error messages when worker fails to start

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 00:52:59 -05:00
Alex Newman a00ca2b3ec fix: ensure worker is running before executing hook logic in multiple scripts 2025-12-09 00:46:23 -05:00
Alex Newman ba2c098ec1 feat: add fs.existsSync import to worker-utils for file existence checks 2025-12-09 00:32:02 -05:00
Alex Newman 5550ecf623 fix: update scripts and hooks for improved worker management and synchronization 2025-12-09 00:25:53 -05:00
Alex Newman 9a27f380c3 docs: lint and publish platform integration guide
- Fix MDX parsing error in search-architecture.mdx (line 382: changed <10ms to "under 10ms")
- Fix broken internal links:
  - architecture-evolution.mdx: VIEWER → /architecture/worker-service
  - usage/private-tags.mdx: search-tools → /usage/search-tools
  - usage/private-tags.mdx: getting-started → /usage/getting-started
- Verify all links pass mintlify broken-links check
- Confirm platform-integration.mdx renders correctly

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-08 22:40:34 -05:00
Alex Newman 577cac8831 Add Platform Integration Guide for claude-mem worker service
- Introduced comprehensive documentation for integrating claude-mem into VSCode extensions, IDE plugins, and CLI tools.
- Detailed worker service basics, including environment variables and build commands.
- Provided an overview of worker architecture and request flow.
- Documented API reference for session lifecycle, data retrieval, search operations, and settings configuration.
- Included integration patterns, error handling strategies, and development workflow guidelines.
- Added critical implementation notes and additional resources for developers.
2025-12-08 22:36:00 -05:00
Alex Newman 8da92c6569 docs: update hooks.mdx with improved architecture diagrams and data flow representation 2025-12-08 21:54:48 -05:00
Alex Newman a18b43744c docs: comprehensive hook lifecycle guide for platform implementers
Rewrote architecture/hooks.mdx with complete technical reference including:
- 5-stage lifecycle overview with ASCII architecture diagram
- Detailed input/output specs for each hook
- Processing steps with TypeScript code examples
- Data flow diagram showing complete request lifecycle
- Session ID threading explanation
- Privacy tag stripping pipeline
- SDK agent processing details
- Implementation checklist for other platforms
- Common pitfalls and solutions table

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 21:49:55 -05:00
Alex Newman 7c4979eba1 docs: update CHANGELOG.md for v7.0.0 2025-12-08 15:17:49 -05:00
Alex Newman ffe1e1622d fix: update context-hook.js and worker-service.cjs for improved functionality and error handling 2025-12-08 15:16:45 -05:00
36 changed files with 4046 additions and 999 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [ "plugins": [
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "7.0.0", "version": "7.0.3",
"source": "./plugin", "source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions" "description": "Persistent memory system for Claude Code - context compression across sessions"
} }
+88
View File
@@ -4,6 +4,94 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [7.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
This major release represents a complete architectural transformation of claude-mem from a monolithic design to a clean, modular HTTP-based architecture.
## Breaking Changes
**None** - Despite being a major version bump due to the scope of changes, this release maintains full backward compatibility. All existing functionality works exactly as before.
## What Changed
### Hooks → HTTP Clients
- All 5 lifecycle hooks converted from direct database access to lightweight HTTP clients
- Each hook reduced from 400-800 lines to ~75 lines
- Hooks now make simple HTTP calls to the worker service
- Eliminates SQL duplication across hooks - single source of truth in worker
### Worker Service Modularization
- `worker-service.ts` reduced from 1600+ lines to clean orchestration layer
- New route-based HTTP architecture:
- `SessionRoutes` - Session lifecycle management
- `DataRoutes` - Database queries (observations, sessions, timeline)
- `SearchRoutes` - Full-text and semantic search
- `SettingsRoutes` - Configuration management
- `ViewerRoutes` - UI endpoints
### New Service Layer
- `BaseRouteHandler` - Centralized error handling, response formatting (used 46x)
- `SessionEventBroadcaster` - Semantic SSE event broadcasting
- `SessionCompletionHandler` - Consolidated session completion logic
- `SettingsDefaultsManager` - Single source of truth for configuration defaults
- `PrivacyCheckValidator` - Centralized privacy tag validation
- `FormattingService` - Dual-format result rendering
- `TimelineService` - Complex markdown timeline formatting
- `SearchManager` - Extracted search logic from context generation
### Database Improvements
- Migrated from \`bun:sqlite\` to \`better-sqlite3\` for broader compatibility
- SQL queries moved from route handlers to \`SessionStore\` for separation of concerns
- \`PaginationHelper\` centralizes paginated queries with LIMIT+1 optimization
### Testing Infrastructure
- New comprehensive happy path tests for full session lifecycle
- Integration tests covering session init, observation capture, search, summaries, cleanup
- Test helpers and mocks for consistent testing patterns
### Type Safety
- Removed 'as any' casts throughout codebase
- New \`src/types/database.ts\` with proper type definitions
- Enhanced null safety in SearchManager
## Stats
- **60 files changed**
- **8,671 insertions, 5,585 deletions**
- Net: ~3,000 lines of new code (mostly tests and new modular services)
## Migration Notes
No migration required! Update and continue using claude-mem as before.
## [6.5.3] - 2025-12-05 ## [6.5.3] - 2025-12-05
## Bug Fixes ## Bug Fixes
+1 -1
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. 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.0 **Current Version**: 7.0.3
## Architecture ## Architecture
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1062,7 +1062,7 @@ The result is a memory system that's both powerful and invisible. Users never no
- [Progressive Disclosure](progressive-disclosure) - The philosophy behind v4 - [Progressive Disclosure](progressive-disclosure) - The philosophy behind v4
- [Hooks Architecture](hooks-architecture) - How hooks power the system - [Hooks Architecture](hooks-architecture) - How hooks power the system
- [Context Engineering](context-engineering) - Foundational principles - [Context Engineering](context-engineering) - Foundational principles
- [Viewer UI](VIEWER) - Real-time visualization (v5.1.0+) - [Worker Service](/architecture/worker-service) - Real-time visualization (v5.1.0+)
--- ---
File diff suppressed because it is too large Load Diff
@@ -379,7 +379,7 @@ Claude translates to appropriate API call.
### 4. Performance ### 4. Performance
**Fast Queries**: FTS5 full-text search <10ms for typical queries **Fast Queries**: FTS5 full-text search under 10ms for typical queries
**Caching**: HTTP layer allows response caching **Caching**: HTTP layer allows response caching
@@ -397,7 +397,7 @@ Claude translates to appropriate API call.
### For Developers ### 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 - Source file kept for reference
- No longer built or registered - No longer built or registered
- MCP configuration removed from `plugin/.mcp.json` - MCP configuration removed from `plugin/.mcp.json`
+1 -1
View File
@@ -82,7 +82,7 @@ ${CLAUDE_PLUGIN_ROOT}/
│ ├── summary-hook.js # Summary generation hook │ ├── summary-hook.js # Summary generation hook
│ ├── cleanup-hook.js # Session cleanup hook │ ├── cleanup-hook.js # Session cleanup hook
│ ├── worker-service.cjs # Worker service (CJS) │ ├── worker-service.cjs # Worker service (CJS)
│ └── search-server.cjs # MCP search server (CJS) │ └── mcp-server.cjs # MCP search server (CJS)
└── ui/ └── ui/
└── viewer.html # Web viewer UI bundle └── viewer.html # Web viewer UI bundle
``` ```
+3 -3
View File
@@ -33,7 +33,7 @@ The build process uses esbuild to compile TypeScript:
1. Compiles TypeScript to JavaScript 1. Compiles TypeScript to JavaScript
2. Creates standalone executables for each hook in `plugin/scripts/` 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` 4. Bundles worker service to `plugin/scripts/worker-service.cjs`
5. Bundles web viewer UI to `plugin/ui/viewer.html` 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) - Hook executables: `*-hook.js` (ESM format)
- Smart installer: `smart-install.js` (ESM format) - Smart installer: `smart-install.js` (ESM format)
- Worker service: `worker-service.cjs` (CJS 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) - Viewer UI: `viewer.html` (self-contained HTML bundle)
### Build Scripts ### Build Scripts
@@ -342,7 +342,7 @@ npm test
### Adding MCP Search Tools ### 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 ```typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => { server.setRequestHandler(CallToolRequestSchema, async (request) => {
+2 -1
View File
@@ -55,7 +55,8 @@
"pages": [ "pages": [
"configuration", "configuration",
"development", "development",
"troubleshooting" "troubleshooting",
"platform-integration"
] ]
}, },
{ {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -542,7 +542,7 @@ The skill includes comprehensive diagnostics, automated repair sequences, and de
2. Verify search server is built: 2. Verify search server is built:
```bash ```bash
ls -l plugin/scripts/search-server.js ls -l plugin/scripts/mcp-server.cjs
``` ```
3. Rebuild if needed: 3. Rebuild if needed:
+2 -2
View File
@@ -165,8 +165,8 @@ This design ensures that private content never reaches the database, search indi
## Related Features ## Related Features
- [Search Tools](search-tools) - How to search past observations - [Search Tools](/usage/search-tools) - How to search past observations
- [Getting Started](getting-started) - Basic usage guide - [Getting Started](/usage/getting-started) - Basic usage guide
- [Configuration](/configuration) - System settings and environment variables - [Configuration](/configuration) - System settings and environment variables
## Troubleshooting ## Troubleshooting
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "7.0.0", "version": "7.0.3",
"description": "Memory compression system for Claude Code - persist context across sessions", "description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [ "keywords": [
"claude", "claude",
@@ -35,7 +35,8 @@
"test:parser": "npx tsx src/sdk/parser.test.ts", "test:parser": "npx tsx src/sdk/parser.test.ts",
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js 2>/dev/null", "test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js 2>/dev/null",
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js", "test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js",
"sync-marketplace": "rsync -av --delete --exclude=.git ./ ~/.claude/plugins/marketplaces/thedotmack/ && cd ~/.claude/plugins/marketplaces/thedotmack/ && npm install", "sync-marketplace": "node scripts/sync-marketplace.cjs",
"sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
"worker:start": "pm2 start ecosystem.config.cjs", "worker:start": "pm2 start ecosystem.config.cjs",
"worker:stop": "pm2 stop claude-mem-worker", "worker:stop": "pm2 stop claude-mem-worker",
"worker:restart": "pm2 restart claude-mem-worker", "worker:restart": "pm2 restart claude-mem-worker",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "7.0.0", "version": "7.0.3",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions", "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": { "author": {
"name": "Alex Newman" "name": "Alex Newman"
+2 -2
View File
@@ -8,12 +8,12 @@
{ {
"type": "command", "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": 5 "timeout": 300
}, },
{ {
"type": "command", "type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js", "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
"timeout": 5 "timeout": 10
} }
] ]
} }
+10 -4
View File
@@ -1,5 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
import{stdin as S}from"process";import h from"path";import{homedir as y}from"os";import{join as o,dirname as C,basename as v}from"path";import{homedir as g}from"os";import{fileURLToPath as D}from"url";function M(){return typeof __dirname<"u"?__dirname:C(D(import.meta.url))}var H=M(),s=process.env.CLAUDE_MEM_DATA_DIR||o(g(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||o(g(),".claude"),X=o(s,"archives"),B=o(s,"logs"),V=o(s,"trash"),$=o(s,"backups"),j=o(s,"settings.json"),G=o(s,"claude-mem.db"),K=o(s,"vector-db"),Y=o(l,"settings.json"),J=o(l,"commands"),q=o(l,"CLAUDE.md");import{readFileSync as R,existsSync as U}from"fs";var N=["bugfix","feature","refactor","discovery","decision","change"],L=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var m=N.join(","),d=L.join(",");var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:m,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!U(t))return this.getAllDefaults();let r=R(t,"utf-8"),n=JSON.parse(r).env||{},i={...this.DEFAULTS};for(let _ of Object.keys(this.DEFAULTS))n[_]!==void 0&&(i[_]=n[_]);return i}};function O(){let e=h.join(y(),".claude-mem","settings.json"),t=p.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}import{appendFileSync as I}from"fs";import{homedir as x}from"os";import{join as P}from"path";var k=P(x(),".claude-mem","silent.log");function c(e,t,r=""){let a=new Date().toISOString(),u=((new Error().stack||"").split(` import{stdin as u}from"process";import T from"path";import{existsSync as f}from"fs";import{homedir as d}from"os";import{spawnSync as U}from"child_process";import{readFileSync as D,existsSync as h}from"fs";var N=["bugfix","feature","refactor","discovery","decision","change"],L=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var p=N.join(","),g=L.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:p,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:g,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let n=this.get(t);return parseInt(n,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!h(t))return this.getAllDefaults();let n=D(t,"utf-8"),o=JSON.parse(n).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))o[i]!==void 0&&(s[i]=o[i]);return s}};var E=T.join(d(),".claude","plugins","marketplaces","thedotmack"),y=100,R=500,I=10;function l(){let e=T.join(d(),".claude-mem","settings.json"),t=_.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function A(){try{let e=l();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(y)})).ok}catch{return!1}}async function P(){try{let e=T.join(E,"ecosystem.config.cjs");if(!f(e))throw new Error(`Ecosystem config not found at ${e}`);let t=T.join(E,"node_modules",".bin","pm2"),n=process.platform==="win32"?t+".cmd":t,r=f(n)?n:"pm2",o=U(r,["start",e],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed");for(let s=0;s<I;s++)if(await new Promise(i=>setTimeout(i,R)),await A())return!0;return!1}catch{return!1}}async function C(){if(await A())return;if(!await P()){let t=l();throw new Error(`Worker service failed to start on port ${t}.
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),A=u?`${u[1].split("/").pop()}:${u[2]}`:"unknown",E=`[${a}] [${A}] ${e}`;if(t!==void 0)try{E+=` ${JSON.stringify(t)}`}catch(T){E+=` [stringify error: ${T}]`}E+=`
`;try{I(k,E)}catch(T){console.error("[silent-debug] Failed to write to log:",T)}return r}async function f(e){c("[cleanup-hook] Hook fired",{session_id:e?.session_id,cwd:e?.cwd,reason:e?.reason}),e||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(` To start manually, run:
Expected 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));let{session_id:t,reason:r}=e,a=O();try{let n=await fetch(`http://127.0.0.1:${a}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:r}),signal:AbortSignal.timeout(2e3)});if(n.ok){let i=await n.json();c("[cleanup-hook] Session cleanup completed",i)}else c("[cleanup-hook] Session not found or already cleaned up")}catch(n){c("[cleanup-hook] Worker not reachable (non-critical)",{error:n.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(S.isTTY)f(void 0);else{let e="";S.on("data",t=>e+=t),S.on("end",async()=>{let t=e?JSON.parse(e):void 0;await f(t)})} cd ${E}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as k}from"fs";import{homedir as w}from"os";import{join as x}from"path";var W=x(w(),".claude-mem","silent.log");function a(e,t,n=""){let r=new Date().toISOString(),S=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),M=S?`${S[1].split("/").pop()}:${S[2]}`:"unknown",c=`[${r}] [${M}] ${e}`;if(t!==void 0)try{c+=` ${JSON.stringify(t)}`}catch(O){c+=` [stringify error: ${O}]`}c+=`
`;try{k(W,c)}catch(O){console.error("[silent-debug] Failed to write to log:",O)}return n}async function m(e){await C(),a("[cleanup-hook] Hook fired",{session_id:e?.session_id,cwd:e?.cwd,reason:e?.reason}),e||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
Expected 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));let{session_id:t,reason:n}=e,r=l();try{let o=await fetch(`http://127.0.0.1:${r}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:n}),signal:AbortSignal.timeout(2e3)});if(o.ok){let s=await o.json();a("[cleanup-hook] Session cleanup completed",s)}else a("[cleanup-hook] Session not found or already cleaned up")}catch(o){a("[cleanup-hook] Worker not reachable (non-critical)",{error:o.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(u.isTTY)m(void 0);else{let e="";u.on("data",t=>e+=t),u.on("end",async()=>{let t=e?JSON.parse(e):void 0;await m(t)})}
+7 -1
View File
@@ -1,2 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
import L from"path";import{stdin as u}from"process";import{execSync as l}from"child_process";import N from"path";import{homedir as R}from"os";import{join as r,dirname as f,basename as x}from"path";import{homedir as T}from"os";import{fileURLToPath as g}from"url";function A(){return typeof __dirname<"u"?__dirname:f(g(import.meta.url))}var v=A(),n=process.env.CLAUDE_MEM_DATA_DIR||r(T(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||r(T(),".claude"),k=r(n,"archives"),b=r(n,"logs"),W=r(n,"trash"),F=r(n,"backups"),H=r(n,"settings.json"),X=r(n,"claude-mem.db"),B=r(n,"vector-db"),V=r(E,"settings.json"),j=r(E,"commands"),G=r(E,"CLAUDE.md");import{readFileSync as d,existsSync as M}from"fs";var C=["bugfix","feature","refactor","discovery","decision","change"],D=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var p=C.join(","),S=D.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:p,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:S,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let o=this.get(t);return parseInt(o,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!M(t))return this.getAllDefaults();let o=d(t,"utf-8"),i=JSON.parse(o).env||{},c={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))i[a]!==void 0&&(c[a]=i[a]);return c}};function m(){let e=N.join(R(),".claude-mem","settings.json"),t=_.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function U(e,t=1e4){let o=Date.now(),s=100;for(;Date.now()-o<t;)try{return l(`curl -s -f -m 1 "http://localhost:${e}/api/health" > /dev/null 2>&1`,{timeout:1e3}),!0}catch{await new Promise(i=>setTimeout(i,s))}return!1}async function O(e){let t=e?.cwd??process.cwd(),o=t?L.basename(t):"unknown-project",s=m();if(!await U(s))throw new Error(`Worker service not available on port ${s} after 10s. Try: npm run worker:restart`);let c=`http://localhost:${s}/api/context/inject?project=${encodeURIComponent(o)}`;return l(`curl -s "${c}"`,{encoding:"utf-8",timeout:5e3}).trim()}var I=process.argv.includes("--colors");if(u.isTTY||I)O(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";u.on("data",t=>e+=t),u.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,o=await O(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:o}})),process.exit(0)})} import R from"path";import{stdin as T}from"process";import{execSync as y}from"child_process";import _ from"path";import{existsSync as u}from"fs";import{homedir as p}from"os";import{spawnSync as m}from"child_process";import{readFileSync as N,existsSync as g}from"fs";var M=["bugfix","feature","refactor","discovery","decision","change"],l=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var O=M.join(","),S=l.join(",");var i=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:O,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:S,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!g(t))return this.getAllDefaults();let r=N(t,"utf-8"),s=JSON.parse(r).env||{},n={...this.DEFAULTS};for(let o of Object.keys(this.DEFAULTS))s[o]!==void 0&&(n[o]=s[o]);return n}};var E=_.join(p(),".claude","plugins","marketplaces","thedotmack"),d=100,D=500,L=10;function a(){let e=_.join(p(),".claude-mem","settings.json"),t=i.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function C(){try{let e=a();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function U(){try{let e=_.join(E,"ecosystem.config.cjs");if(!u(e))throw new Error(`Ecosystem config not found at ${e}`);let t=_.join(E,"node_modules",".bin","pm2"),r=process.platform==="win32"?t+".cmd":t,c=u(r)?r:"pm2",s=m(c,["start",e],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw new Error(s.stderr||"PM2 start failed");for(let n=0;n<L;n++)if(await new Promise(o=>setTimeout(o,D)),await C())return!0;return!1}catch{return!1}}async function A(){if(await C())return;if(!await U()){let t=a();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${E}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}async function f(e){await A();let t=e?.cwd??process.cwd(),r=t?R.basename(t):"unknown-project",s=`http://127.0.0.1:${a()}/api/context/inject?project=${encodeURIComponent(r)}`;return y(`curl -s "${s}"`,{encoding:"utf-8",timeout:5e3}).trim()}var I=process.argv.includes("--colors");if(T.isTTY||I)f(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";T.on("data",t=>e+=t),T.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,r=await f(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import _e from"path";import{stdin as H}from"process";import K from"better-sqlite3";import{join as m,dirname as j,basename as le}from"path";import{homedir as v}from"os";import{existsSync as be,mkdirSync as $}from"fs";import{fileURLToPath as G}from"url";function Y(){return typeof __dirname<"u"?__dirname:j(G(import.meta.url))}var V=Y(),l=process.env.CLAUDE_MEM_DATA_DIR||m(v(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(v(),".claude"),Oe=m(l,"archives"),he=m(l,"logs"),Ne=m(l,"trash"),fe=m(l,"backups"),Ie=m(l,"settings.json"),y=m(l,"claude-mem.db"),Ae=m(l,"vector-db"),Le=m(N,"settings.json"),Ce=m(N,"commands"),De=m(N,"CLAUDE.md");function k(a){$(a,{recursive:!0})}function f(){return m(V,"..","..")}var I=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(I||{}),A=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=I[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} import _e from"path";import{stdin as W}from"process";import q from"better-sqlite3";import{join as m,dirname as G,basename as le}from"path";import{homedir as v}from"os";import{existsSync as be,mkdirSync as Y}from"fs";import{fileURLToPath as V}from"url";function K(){return typeof __dirname<"u"?__dirname:G(V(import.meta.url))}var Oe=K(),l=process.env.CLAUDE_MEM_DATA_DIR||m(v(),".claude-mem"),I=process.env.CLAUDE_CONFIG_DIR||m(v(),".claude"),he=m(l,"archives"),Ne=m(l,"logs"),fe=m(l,"trash"),Ie=m(l,"backups"),Ae=m(l,"settings.json"),y=m(l,"claude-mem.db"),Le=m(l,"vector-db"),Ce=m(I,"settings.json"),De=m(I,"commands"),ve=m(I,"CLAUDE.md");function k(a){Y(a,{recursive:!0})}var A=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),L=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=A[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let n=new Date().toISOString().replace("T"," ").substring(0,23),i=I[e].padEnd(5),p=s.padEnd(6),d="";r?.correlationId?d=`[${r.correlationId}] `:r?.sessionId&&(d=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=` ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let n=new Date().toISOString().replace("T"," ").substring(0,23),i=A[e].padEnd(5),p=s.padEnd(6),d="";r?.correlationId?d=`[${r.correlationId}] `:r?.sessionId&&(d=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:S,sdkSessionId:b,correlationId:u,...c}=r;Object.keys(c).length>0&&(_=` {${Object.entries(c).map(([B,W])=>`${B}=${W}`).join(", ")}}`)}let T=`[${n}] [${i}] [${p}] ${d}${t}${_}${E}`;e===3?console.error(T):console.log(T)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},x=new A;var R=class{db;constructor(){k(l),this.db=new K(y),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` `+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:S,sdkSessionId:b,correlationId:u,...c}=r;Object.keys(c).length>0&&(_=` {${Object.entries(c).map(([j,$])=>`${j}=${$}`).join(", ")}}`)}let T=`[${n}] [${i}] [${p}] ${d}${t}${_}${E}`;e===3?console.error(T):console.log(T)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},M=new L;var R=class{db;constructor(){k(l),this.db=new q(y),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions ( CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL, version INTEGER UNIQUE NOT NULL,
@@ -314,7 +314,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions UPDATE sdk_sessions
SET sdk_session_id = ? SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL WHERE id = ? AND sdk_session_id IS NULL
`).run(s,e).changes===0?(x.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(` `).run(s,e).changes===0?(M.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
UPDATE sdk_sessions UPDATE sdk_sessions
SET worker_port = ? SET worker_port = ?
WHERE id = ? WHERE id = ?
@@ -417,12 +417,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")} WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC ORDER BY up.created_at_epoch ASC
`;try{let S=this.db.prepare(E).all(p,d,...i),b=this.db.prepare(_).all(p,d,...i),u=this.db.prepare(T).all(p,d,...i);return{observations:S,sessions:b.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:u.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(S){return console.error("[SessionStore] Error querying timeline records:",S.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function q(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function L(a,e,s={}){let t=q(a,e,s);return JSON.stringify(t)}import C from"path";import{homedir as ee}from"os";import{spawnSync as se}from"child_process";import{readFileSync as z,existsSync as Z}from"fs";var J=["bugfix","feature","refactor","discovery","decision","change"],Q=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var U=J.join(","),M=Q.join(",");var O=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:U,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:M,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(e){return process.env[e]||this.DEFAULTS[e]}static getInt(e){let s=this.get(e);return parseInt(s,10)}static getBool(e){return this.get(e)==="true"}static loadFromFile(e){if(!Z(e))return this.getAllDefaults();let s=z(e,"utf-8"),r=JSON.parse(s).env||{},o={...this.DEFAULTS};for(let n of Object.keys(this.DEFAULTS))r[n]!==void 0&&(o[n]=r[n]);return o}};var te=100,re=500,oe=10;function h(){let a=C.join(ee(),".claude-mem","settings.json"),e=O.loadFromFile(a);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}async function w(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(te)})).ok}catch{return!1}}async function ne(){try{let a=f(),e=C.join(a,"ecosystem.config.cjs");if(!existsSync(e))throw new Error(`Ecosystem config not found at ${e}`);let s=C.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=existsSync(t)?t:"pm2",o=se(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed");for(let n=0;n<oe;n++)if(await new Promise(i=>setTimeout(i,re)),await w())return!0;return!1}catch{return!1}}async function F(){if(await w())return;if(!await ne()){let e=h(),s=f();throw new Error(`Worker service failed to start on port ${e}. `;try{let S=this.db.prepare(E).all(p,d,...i),b=this.db.prepare(_).all(p,d,...i),u=this.db.prepare(T).all(p,d,...i);return{observations:S,sessions:b.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:u.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(S){return console.error("[SessionStore] Error querying timeline records:",S.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function J(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(a,e,s={}){let t=J(a,e,s);return JSON.stringify(t)}import N from"path";import{existsSync as w}from"fs";import{homedir as F}from"os";import{spawnSync as se}from"child_process";import{readFileSync as Z,existsSync as ee}from"fs";var Q=["bugfix","feature","refactor","discovery","decision","change"],z=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var U=Q.join(","),x=z.join(",");var O=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:U,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:x,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(e){return process.env[e]||this.DEFAULTS[e]}static getInt(e){let s=this.get(e);return parseInt(s,10)}static getBool(e){return this.get(e)==="true"}static loadFromFile(e){if(!ee(e))return this.getAllDefaults();let s=Z(e,"utf-8"),r=JSON.parse(s).env||{},o={...this.DEFAULTS};for(let n of Object.keys(this.DEFAULTS))r[n]!==void 0&&(o[n]=r[n]);return o}};var h=N.join(F(),".claude","plugins","marketplaces","thedotmack"),te=100,re=500,oe=10;function f(){let a=N.join(F(),".claude-mem","settings.json"),e=O.loadFromFile(a);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}async function X(){try{let a=f();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(te)})).ok}catch{return!1}}async function ne(){try{let a=N.join(h,"ecosystem.config.cjs");if(!w(a))throw new Error(`Ecosystem config not found at ${a}`);let e=N.join(h,"node_modules",".bin","pm2"),s=process.platform==="win32"?e+".cmd":e,t=w(s)?s:"pm2",r=se(t,["start",a],{cwd:h,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed");for(let o=0;o<oe;o++)if(await new Promise(n=>setTimeout(n,re)),await X())return!0;return!1}catch{return!1}}async function P(){if(await X())return;if(!await ne()){let e=f();throw new Error(`Worker service failed to start on port ${e}.
To start manually, run: To start manually, run:
cd ${s} cd ${h}
npx pm2 start ecosystem.config.cjs npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as ie}from"fs";import{homedir as ae}from"os";import{join as pe}from"path";var ce=pe(ae(),".claude-mem","silent.log");function g(a,e,s=""){let t=new Date().toISOString(),i=((new Error().stack||"").split(` If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as ie}from"fs";import{homedir as ae}from"os";import{join as pe}from"path";var ce=pe(ae(),".claude-mem","silent.log");function g(a,e,s=""){let t=new Date().toISOString(),i=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),p=i?`${i[1].split("/").pop()}:${i[2]}`:"unknown",d=`[${t}] [${p}] ${a}`;if(e!==void 0)try{d+=` ${JSON.stringify(e)}`}catch(E){d+=` [stringify error: ${E}]`}d+=` `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),p=i?`${i[1].split("/").pop()}:${i[2]}`:"unknown",d=`[${t}] [${p}] ${a}`;if(e!==void 0)try{d+=` ${JSON.stringify(e)}`}catch(E){d+=` [stringify error: ${E}]`}d+=`
`;try{ie(ce,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var X=100;function de(a){let e=(a.match(/<private>/g)||[]).length,s=(a.match(/<claude-mem-context>/g)||[]).length;return e+s}function P(a){if(typeof a!="string")return g("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=de(a);return e>X&&g("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:X,contentLength:a.length}),a.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g,"").replace(/<private>[\s\S]*?<\/private>/g,"").trim()}async function ue(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;g("[new-hook] Input received",{session_id:e,cwd:s,cwd_type:typeof s,cwd_length:s?.length,has_cwd:!!s,prompt_length:t?.length});let r=_e.basename(s);g("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s}),await F();let o=new R,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=P(t);if(!p||p.trim()===""){g("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:i,originalLength:t.length}),o.close(),console.error(`[new-hook] Session ${n}, prompt #${i} (fully private - skipped)`),console.log(L("UserPromptSubmit",!0));return}o.saveUserPrompt(e,i,p),console.error(`[new-hook] Session ${n}, prompt #${i}`),o.close();let d=h(),E=t.startsWith("/")?t.substring(1):t;try{let _=await fetch(`http://127.0.0.1:${d}/sessions/${n}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:E,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!_.ok){let T=await _.text();throw new Error(`Failed to initialize session: ${_.status} ${T}`)}}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):_}console.log(L("UserPromptSubmit",!0))}var D="";H.on("data",a=>D+=a);H.on("end",async()=>{let a=D?JSON.parse(D):void 0;await ue(a)}); `;try{ie(ce,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var H=100;function de(a){let e=(a.match(/<private>/g)||[]).length,s=(a.match(/<claude-mem-context>/g)||[]).length;return e+s}function B(a){if(typeof a!="string")return g("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=de(a);return e>H&&g("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:H,contentLength:a.length}),a.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g,"").replace(/<private>[\s\S]*?<\/private>/g,"").trim()}async function ue(a){if(await P(),!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;g("[new-hook] Input received",{session_id:e,cwd:s,cwd_type:typeof s,cwd_length:s?.length,has_cwd:!!s,prompt_length:t?.length});let r=_e.basename(s);g("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s});let o=new R,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=B(t);if(!p||p.trim()===""){g("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:i,originalLength:t.length}),o.close(),console.error(`[new-hook] Session ${n}, prompt #${i} (fully private - skipped)`),console.log(C("UserPromptSubmit",!0));return}o.saveUserPrompt(e,i,p),console.error(`[new-hook] Session ${n}, prompt #${i}`),o.close();let d=f(),E=t.startsWith("/")?t.substring(1):t;try{let _=await fetch(`http://127.0.0.1:${d}/sessions/${n}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:E,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!_.ok){let T=await _.text();throw new Error(`Failed to initialize session: ${_.status} ${T}`)}}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):_}console.log(C("UserPromptSubmit",!0))}var D="";W.on("data",a=>D+=a);W.on("end",async()=>{let a=D?JSON.parse(D):void 0;await ue(a)});
+5 -5
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env node #!/usr/bin/env node
import{stdin as I}from"process";function v(n,t,e){return n==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function S(n,t,e={}){let o=v(n,t,e);return JSON.stringify(o)}var T=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(T||{}),O=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message} import{stdin as U}from"process";function P(r,t,e){return r==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function S(r,t,e={}){let o=P(r,t,e);return JSON.stringify(o)}var T=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(T||{}),g=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let o=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&o.command){let r=o.command.length>50?o.command.substring(0,50)+"...":o.command;return`${t}(${r})`}if(t==="Read"&&o.file_path){let r=o.file_path.split("/").pop()||o.file_path;return`${t}(${r})`}if(t==="Edit"&&o.file_path){let r=o.file_path.split("/").pop()||o.file_path;return`${t}(${r})`}if(t==="Write"&&o.file_path){let r=o.file_path.split("/").pop()||o.file_path;return`${t}(${r})`}return t}catch{return t}}log(t,e,o,r,s){if(t<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),_=T[t].padEnd(5),c=e.padEnd(6),p="";r?.correlationId?p=`[${r.correlationId}] `:r?.sessionId&&(p=`[session-${r.sessionId}] `);let g="";s!=null&&(this.level===0&&typeof s=="object"?g=` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let o=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&o.command){let n=o.command.length>50?o.command.substring(0,50)+"...":o.command;return`${t}(${n})`}if(t==="Read"&&o.file_path){let n=o.file_path.split("/").pop()||o.file_path;return`${t}(${n})`}if(t==="Edit"&&o.file_path){let n=o.file_path.split("/").pop()||o.file_path;return`${t}(${n})`}if(t==="Write"&&o.file_path){let n=o.file_path.split("/").pop()||o.file_path;return`${t}(${n})`}return t}catch{return t}}log(t,e,o,n,s){if(t<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),f=T[t].padEnd(5),i=e.padEnd(6),u="";n?.correlationId?u=`[${n.correlationId}] `:n?.sessionId&&(u=`[session-${n.sessionId}] `);let O="";s!=null&&(this.level===0&&typeof s=="object"?O=`
`+JSON.stringify(s,null,2):g=" "+this.formatData(s));let D="";if(r){let{sessionId:Q,sdkSessionId:z,correlationId:Z,...y}=r;Object.keys(y).length>0&&(D=` {${Object.entries(y).map(([x,P])=>`${x}=${P}`).join(", ")}}`)}let R=`[${a}] [${_}] [${c}] ${p}${o}${D}${g}`;t===3?console.error(R):console.log(R)}debug(t,e,o,r){this.log(0,t,e,o,r)}info(t,e,o,r){this.log(1,t,e,o,r)}warn(t,e,o,r){this.log(2,t,e,o,r)}error(t,e,o,r){this.log(3,t,e,o,r)}dataIn(t,e,o,r){this.info(t,`\u2192 ${e}`,o,r)}dataOut(t,e,o,r){this.info(t,`\u2190 ${e}`,o,r)}success(t,e,o,r){this.info(t,`\u2713 ${e}`,o,r)}failure(t,e,o,r){this.error(t,`\u2717 ${e}`,o,r)}timing(t,e,o,r){this.info(t,`\u23F1 ${e}`,r,{duration:`${o}ms`})}},E=new O;import C from"path";import{homedir as X}from"os";import{spawnSync as j}from"child_process";import{join as i,dirname as k,basename as nt}from"path";import{homedir as h}from"os";import{fileURLToPath as b}from"url";function w(){return typeof __dirname<"u"?__dirname:k(b(import.meta.url))}var $=w(),u=process.env.CLAUDE_MEM_DATA_DIR||i(h(),".claude-mem"),m=process.env.CLAUDE_CONFIG_DIR||i(h(),".claude"),ct=i(u,"archives"),ut=i(u,"logs"),pt=i(u,"trash"),_t=i(u,"backups"),Et=i(u,"settings.json"),lt=i(u,"claude-mem.db"),ft=i(u,"vector-db"),gt=i(m,"settings.json"),St=i(m,"commands"),Tt=i(m,"CLAUDE.md");function d(){return i($,"..","..")}import{readFileSync as F,existsSync as B}from"fs";var H=["bugfix","feature","refactor","discovery","decision","change"],W=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var L=H.join(","),M=W.join(",");var l=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:L,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:M,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!B(t))return this.getAllDefaults();let e=F(t,"utf-8"),r=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))r[a]!==void 0&&(s[a]=r[a]);return s}};var K=100,V=500,G=10;function f(){let n=C.join(X(),".claude-mem","settings.json"),t=l.loadFromFile(n);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function N(){try{let n=f();return(await fetch(`http://127.0.0.1:${n}/health`,{signal:AbortSignal.timeout(K)})).ok}catch{return!1}}async function Y(){try{let n=d(),t=C.join(n,"ecosystem.config.cjs");if(!existsSync(t))throw new Error(`Ecosystem config not found at ${t}`);let e=C.join(n,"node_modules",".bin","pm2"),o=process.platform==="win32"?e+".cmd":e,r=existsSync(o)?o:"pm2",s=j(r,["start",t],{cwd:n,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw new Error(s.stderr||"PM2 start failed");for(let a=0;a<G;a++)if(await new Promise(_=>setTimeout(_,V)),await N())return!0;return!1}catch{return!1}}async function U(){if(await N())return;if(!await Y()){let t=f(),e=d();throw new Error(`Worker service failed to start on port ${t}. `+JSON.stringify(s,null,2):O=" "+this.formatData(s));let C="";if(n){let{sessionId:K,sdkSessionId:j,correlationId:V,...A}=n;Object.keys(A).length>0&&(C=` {${Object.entries(A).map(([D,I])=>`${D}=${I}`).join(", ")}}`)}let d=`[${a}] [${f}] [${i}] ${u}${o}${C}${O}`;t===3?console.error(d):console.log(d)}debug(t,e,o,n){this.log(0,t,e,o,n)}info(t,e,o,n){this.log(1,t,e,o,n)}warn(t,e,o,n){this.log(2,t,e,o,n)}error(t,e,o,n){this.log(3,t,e,o,n)}dataIn(t,e,o,n){this.info(t,`\u2192 ${e}`,o,n)}dataOut(t,e,o,n){this.info(t,`\u2190 ${e}`,o,n)}success(t,e,o,n){this.info(t,`\u2713 ${e}`,o,n)}failure(t,e,o,n){this.error(t,`\u2717 ${e}`,o,n)}timing(t,e,o,n){this.info(t,`\u23F1 ${e}`,n,{duration:`${o}ms`})}},c=new g;import _ from"path";import{existsSync as h}from"fs";import{homedir as R}from"os";import{spawnSync as x}from"child_process";import{readFileSync as v,existsSync as w}from"fs";var b=["bugfix","feature","refactor","discovery","decision","change"],k=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var y=b.join(","),M=k.join(",");var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:y,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:M,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!w(t))return this.getAllDefaults();let e=v(t,"utf-8"),n=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(s[a]=n[a]);return s}};var E=_.join(R(),".claude","plugins","marketplaces","thedotmack"),$=100,H=500,W=10;function l(){let r=_.join(R(),".claude-mem","settings.json"),t=p.loadFromFile(r);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function L(){try{let r=l();return(await fetch(`http://127.0.0.1:${r}/health`,{signal:AbortSignal.timeout($)})).ok}catch{return!1}}async function F(){try{let r=_.join(E,"ecosystem.config.cjs");if(!h(r))throw new Error(`Ecosystem config not found at ${r}`);let t=_.join(E,"node_modules",".bin","pm2"),e=process.platform==="win32"?t+".cmd":t,o=h(e)?e:"pm2",n=x(o,["start",r],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let s=0;s<W;s++)if(await new Promise(a=>setTimeout(a,H)),await L())return!0;return!1}catch{return!1}}async function N(){if(await L())return;if(!await F()){let t=l();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run: To start manually, run:
cd ${e} cd ${E}
npx pm2 start ecosystem.config.cjs npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}var J=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function q(n){if(!n)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:r,tool_response:s}=n;if(J.has(o)){console.log(S("PostToolUse",!0));return}await U();let a=f(),_=E.formatTool(o,r);E.dataIn("HOOK",`PostToolUse: ${_}`,{workerPort:a});try{let c=await fetch(`http://127.0.0.1:${a}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:o,tool_input:r,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let p=await c.text();throw E.failure("HOOK","Failed to send observation",{status:c.status},p),new Error(`Failed to send observation to worker: ${c.status} ${p}`)}E.debug("HOOK","Observation sent successfully",{toolName:o})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(S("PostToolUse",!0))}var A="";I.on("data",n=>A+=n);I.on("end",async()=>{let n=A?JSON.parse(A):void 0;await q(n)}); If already running, try: npx pm2 restart claude-mem-worker`)}}var X=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function B(r){if(await N(),!r)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:n,tool_response:s}=r;if(X.has(o)){console.log(S("PostToolUse",!0));return}let a=l(),f=c.formatTool(o,n);c.dataIn("HOOK",`PostToolUse: ${f}`,{workerPort:a});try{let i=await fetch(`http://127.0.0.1:${a}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:o,tool_input:n,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let u=await i.text();throw c.failure("HOOK","Failed to send observation",{status:i.status},u),new Error(`Failed to send observation to worker: ${i.status} ${u}`)}c.debug("HOOK","Observation sent successfully",{toolName:o})}catch(i){throw i.cause?.code==="ECONNREFUSED"||i.name==="TimeoutError"||i.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):i}console.log(S("PostToolUse",!0))}var m="";U.on("data",r=>m+=r);U.on("end",async()=>{let r=m?JSON.parse(m):void 0;await B(r)});
File diff suppressed because one or more lines are too long
+9 -9
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env node #!/usr/bin/env node
import{stdin as U}from"process";import{readFileSync as I,existsSync as P}from"fs";function b(o,t,e){return o==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function R(o,t,e={}){let r=b(o,t,e);return JSON.stringify(r)}var m=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(m||{}),S=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=m[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message} import{stdin as D}from"process";import{readFileSync as U,existsSync as I}from"fs";function k(o,t,e){return o==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function d(o,t,e={}){let n=k(o,t,e);return JSON.stringify(n)}var O=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(O||{}),S=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let r=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&r.command){let n=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${t}(${n})`}if(t==="Read"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}if(t==="Edit"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}if(t==="Write"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}return t}catch{return t}}log(t,e,r,n,s){if(t<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),c=m[t].padEnd(5),f=e.padEnd(6),g="";n?.correlationId?g=`[${n.correlationId}] `:n?.sessionId&&(g=`[session-${n.sessionId}] `);let l="";s!=null&&(this.level===0&&typeof s=="object"?l=` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let n=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&n.command){let r=n.command.length>50?n.command.substring(0,50)+"...":n.command;return`${t}(${r})`}if(t==="Read"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}if(t==="Edit"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}if(t==="Write"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}return t}catch{return t}}log(t,e,n,r,s){if(t<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=O[t].padEnd(5),u=e.padEnd(6),l="";r?.correlationId?l=`[${r.correlationId}] `:r?.sessionId&&(l=`[session-${r.sessionId}] `);let g="";s!=null&&(this.level===0&&typeof s=="object"?g=`
`+JSON.stringify(s,null,2):l=" "+this.formatData(s));let y="";if(n){let{sessionId:tt,sdkSessionId:et,correlationId:rt,...D}=n;Object.keys(D).length>0&&(y=` {${Object.entries(D).map(([k,v])=>`${k}=${v}`).join(", ")}}`)}let A=`[${i}] [${c}] [${f}] ${g}${r}${y}${l}`;t===3?console.error(A):console.log(A)}debug(t,e,r,n){this.log(0,t,e,r,n)}info(t,e,r,n){this.log(1,t,e,r,n)}warn(t,e,r,n){this.log(2,t,e,r,n)}error(t,e,r,n){this.log(3,t,e,r,n)}dataIn(t,e,r,n){this.info(t,`\u2192 ${e}`,r,n)}dataOut(t,e,r,n){this.info(t,`\u2190 ${e}`,r,n)}success(t,e,r,n){this.info(t,`\u2713 ${e}`,r,n)}failure(t,e,r,n){this.error(t,`\u2717 ${e}`,r,n)}timing(t,e,r,n){this.info(t,`\u23F1 ${e}`,n,{duration:`${r}ms`})}},p=new S;import d from"path";import{homedir as K}from"os";import{spawnSync as V}from"child_process";import{join as a,dirname as w,basename as at}from"path";import{homedir as h}from"os";import{fileURLToPath as $}from"url";function H(){return typeof __dirname<"u"?__dirname:w($(import.meta.url))}var F=H(),u=process.env.CLAUDE_MEM_DATA_DIR||a(h(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||a(h(),".claude"),ft=a(u,"archives"),Et=a(u,"logs"),_t=a(u,"trash"),gt=a(u,"backups"),lt=a(u,"settings.json"),mt=a(u,"claude-mem.db"),St=a(u,"vector-db"),Ot=a(O,"settings.json"),Tt=a(O,"commands"),dt=a(O,"CLAUDE.md");function T(){return a(F,"..","..")}import{readFileSync as B,existsSync as X}from"fs";var W=["bugfix","feature","refactor","discovery","decision","change"],j=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var M=W.join(","),L=j.join(",");var E=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:M,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:L,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!X(t))return this.getAllDefaults();let e=B(t,"utf-8"),n=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(s[i]=n[i]);return s}};var G=100,Y=500,J=10;function _(){let o=d.join(K(),".claude-mem","settings.json"),t=E.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function N(){try{let o=_();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(G)})).ok}catch{return!1}}async function q(){try{let o=T(),t=d.join(o,"ecosystem.config.cjs");if(!existsSync(t))throw new Error(`Ecosystem config not found at ${t}`);let e=d.join(o,"node_modules",".bin","pm2"),r=process.platform==="win32"?e+".cmd":e,n=existsSync(r)?r:"pm2",s=V(n,["start",t],{cwd:o,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw new Error(s.stderr||"PM2 start failed");for(let i=0;i<J;i++)if(await new Promise(c=>setTimeout(c,Y)),await N())return!0;return!1}catch{return!1}}async function x(){if(await N())return;if(!await q()){let t=_(),e=T();throw new Error(`Worker service failed to start on port ${t}. `+JSON.stringify(s,null,2):g=" "+this.formatData(s));let T="";if(r){let{sessionId:G,sdkSessionId:Y,correlationId:J,...C}=r;Object.keys(C).length>0&&(T=` {${Object.entries(C).map(([x,b])=>`${x}=${b}`).join(", ")}}`)}let y=`[${i}] [${a}] [${u}] ${l}${n}${T}${g}`;t===3?console.error(y):console.log(y)}debug(t,e,n,r){this.log(0,t,e,n,r)}info(t,e,n,r){this.log(1,t,e,n,r)}warn(t,e,n,r){this.log(2,t,e,n,r)}error(t,e,n,r){this.log(3,t,e,n,r)}dataIn(t,e,n,r){this.info(t,`\u2192 ${e}`,n,r)}dataOut(t,e,n,r){this.info(t,`\u2190 ${e}`,n,r)}success(t,e,n,r){this.info(t,`\u2713 ${e}`,n,r)}failure(t,e,n,r){this.error(t,`\u2717 ${e}`,n,r)}timing(t,e,n,r){this.info(t,`\u23F1 ${e}`,r,{duration:`${n}ms`})}},c=new S;import f from"path";import{existsSync as M}from"fs";import{homedir as R}from"os";import{spawnSync as H}from"child_process";import{readFileSync as w,existsSync as $}from"fs";var P=["bugfix","feature","refactor","discovery","decision","change"],v=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var A=P.join(","),h=v.join(",");var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:A,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:h,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!$(t))return this.getAllDefaults();let e=w(t,"utf-8"),r=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))r[i]!==void 0&&(s[i]=r[i]);return s}};var E=f.join(R(),".claude","plugins","marketplaces","thedotmack"),W=100,F=500,X=10;function _(){let o=f.join(R(),".claude-mem","settings.json"),t=p.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function L(){try{let o=_();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function j(){try{let o=f.join(E,"ecosystem.config.cjs");if(!M(o))throw new Error(`Ecosystem config not found at ${o}`);let t=f.join(E,"node_modules",".bin","pm2"),e=process.platform==="win32"?t+".cmd":t,n=M(e)?e:"pm2",r=H(n,["start",o],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed");for(let s=0;s<X;s++)if(await new Promise(i=>setTimeout(i,F)),await L())return!0;return!1}catch{return!1}}async function N(){if(await L())return;if(!await j()){let t=_();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run: To start manually, run:
cd ${e} cd ${E}
npx pm2 start ecosystem.config.cjs npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}function z(o){if(!o||!P(o))return"";try{let t=I(o,"utf-8").trim();if(!t)return"";let e=t.split(` If already running, try: npx pm2 restart claude-mem-worker`)}}function B(o){if(!o||!I(o))return"";try{let t=U(o,"utf-8").trim();if(!t)return"";let e=t.split(`
`);for(let r=e.length-1;r>=0;r--)try{let n=JSON.parse(e[r]);if(n.type==="user"&&n.message?.content){let s=n.message.content;if(typeof s=="string")return s;if(Array.isArray(s))return s.filter(c=>c.type==="text").map(c=>c.text).join(` `);for(let n=e.length-1;n>=0;n--)try{let r=JSON.parse(e[n]);if(r.type==="user"&&r.message?.content){let s=r.message.content;if(typeof s=="string")return s;if(Array.isArray(s))return s.filter(a=>a.type==="text").map(a=>a.text).join(`
`)}}catch{continue}}catch(t){p.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}function Q(o){if(!o||!P(o))return"";try{let t=I(o,"utf-8").trim();if(!t)return"";let e=t.split(` `)}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}function K(o){if(!o||!I(o))return"";try{let t=U(o,"utf-8").trim();if(!t)return"";let e=t.split(`
`);for(let r=e.length-1;r>=0;r--)try{let n=JSON.parse(e[r]);if(n.type==="assistant"&&n.message?.content){let s="",i=n.message.content;return typeof i=="string"?s=i:Array.isArray(i)&&(s=i.filter(f=>f.type==="text").map(f=>f.text).join(` `);for(let n=e.length-1;n>=0;n--)try{let r=JSON.parse(e[n]);if(r.type==="assistant"&&r.message?.content){let s="",i=r.message.content;return typeof i=="string"?s=i:Array.isArray(i)&&(s=i.filter(u=>u.type==="text").map(u=>u.text).join(`
`)),s=s.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),s=s.replace(/\n{3,}/g,` `)),s=s.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),s=s.replace(/\n{3,}/g,`
`).trim(),s}}catch{continue}}catch(t){p.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function Z(o){if(!o)throw new Error("summaryHook requires input");let{session_id:t}=o;await x();let e=_(),r=z(o.transcript_path||""),n=Q(o.transcript_path||"");p.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!r,hasLastAssistantMessage:!!n});try{let s=await fetch(`http://127.0.0.1:${e}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:r,last_assistant_message:n}),signal:AbortSignal.timeout(2e3)});if(!s.ok){let i=await s.text();throw p.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}p.debug("HOOK","Summary request sent successfully")}catch(s){throw s.cause?.code==="ECONNREFUSED"||s.name==="TimeoutError"||s.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):s}finally{fetch(`http://127.0.0.1:${e}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})}).catch(()=>{})}console.log(R("Stop",!0))}var C="";U.on("data",o=>C+=o);U.on("end",async()=>{let o=C?JSON.parse(C):void 0;await Z(o)}); `).trim(),s}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function V(o){if(await N(),!o)throw new Error("summaryHook requires input");let{session_id:t}=o,e=_(),n=B(o.transcript_path||""),r=K(o.transcript_path||"");c.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!n,hasLastAssistantMessage:!!r});try{let s=await fetch(`http://127.0.0.1:${e}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:n,last_assistant_message:r}),signal:AbortSignal.timeout(2e3)});if(!s.ok){let i=await s.text();throw c.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}c.debug("HOOK","Summary request sent successfully")}catch(s){throw s.cause?.code==="ECONNREFUSED"||s.name==="TimeoutError"||s.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):s}finally{fetch(`http://127.0.0.1:${e}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})}).catch(()=>{})}console.log(d("Stop",!0))}var m="";D.on("data",o=>m+=o);D.on("end",async()=>{let o=m?JSON.parse(m):void 0;await V(o)});
+14 -8
View File
@@ -1,5 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
import{join as O,basename as x}from"path";import{homedir as P}from"os";import{existsSync as k}from"fs";import I from"path";import{homedir as w}from"os";import{join as e,dirname as M,basename as X}from"path";import{homedir as l}from"os";import{fileURLToPath as h}from"url";function N(){return typeof __dirname<"u"?__dirname:M(h(import.meta.url))}var G=N(),s=process.env.CLAUDE_MEM_DATA_DIR||e(l(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||e(l(),".claude"),K=e(s,"archives"),Y=e(s,"logs"),$=e(s,"trash"),q=e(s,"backups"),J=e(s,"settings.json"),Z=e(s,"claude-mem.db"),z=e(s,"vector-db"),Q=e(E,"settings.json"),tt=e(E,"commands"),et=e(E,"CLAUDE.md");import{readFileSync as R,existsSync as y}from"fs";var U=["bugfix","feature","refactor","discovery","decision","change"],L=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var S=U.join(","),d=L.join(",");var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:S,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!y(t))return this.getAllDefaults();let r=R(t,"utf-8"),o=JSON.parse(r).env||{},a={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))o[i]!==void 0&&(a[i]=o[i]);return a}};function g(){let n=I.join(w(),".claude-mem","settings.json"),t=c.loadFromFile(n);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}var v=O(P(),".claude","plugins","marketplaces","thedotmack"),b=O(v,"node_modules");k(b)||(console.error(` import{join as M,basename as k}from"path";import{homedir as v}from"os";import{existsSync as x}from"fs";import c from"path";import{existsSync as m}from"fs";import{homedir as C}from"os";import{spawnSync as R}from"child_process";import{readFileSync as L,existsSync as y}from"fs";var h=["bugfix","feature","refactor","discovery","decision","change"],U=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var p=h.join(","),S=U.join(",");var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",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_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:p,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:S,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let n=this.get(t);return parseInt(n,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!y(t))return this.getAllDefaults();let n=L(t,"utf-8"),o=JSON.parse(n).env||{},r={...this.DEFAULTS};for(let s of Object.keys(this.DEFAULTS))o[s]!==void 0&&(r[s]=o[s]);return r}};var E=c.join(C(),".claude","plugins","marketplaces","thedotmack"),w=100,I=500,P=10;function _(){let e=c.join(C(),".claude-mem","settings.json"),t=a.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function f(){try{let e=_();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(w)})).ok}catch{return!1}}async function W(){try{let e=c.join(E,"ecosystem.config.cjs");if(!m(e))throw new Error(`Ecosystem config not found at ${e}`);let t=c.join(E,"node_modules",".bin","pm2"),n=process.platform==="win32"?t+".cmd":t,i=m(n)?n:"pm2",o=R(i,["start",e],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed");for(let r=0;r<P;r++)if(await new Promise(s=>setTimeout(s,I)),await f())return!0;return!1}catch{return!1}}async function A(){if(await f())return;if(!await W()){let t=_();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${E}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}var b=M(v(),".claude","plugins","marketplaces","thedotmack"),X=M(b,"node_modules");x(X)||(console.error(`
--- ---
\u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for \u{1F389} 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. user messages in Claude Code UI until a better method is provided.
@@ -17,7 +23,7 @@ Dependencies have been installed in the background. This only happens once.
Thank you for installing Claude-Mem! Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal. This message was not added to your startup context, so you can continue working as normal.
`),process.exit(3));try{let n=g(),t=x(process.cwd()),r=await fetch(`http://127.0.0.1:${n}/api/context/inject?project=${encodeURIComponent(t)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!r.ok)throw new Error(`Worker error ${r.status}`);let u=await r.text(),o=new Date,a=new Date("2025-12-06T00:00:00Z"),i=new Date("2025-12-05T05:00:00Z"),T="";o<i&&(T=` `),process.exit(3));try{await A();let e=_(),t=k(process.cwd()),n=await fetch(`http://127.0.0.1:${e}/api/context/inject?project=${encodeURIComponent(t)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`Worker error ${n.status}`);let i=await n.text(),o=new Date,r=new Date("2025-12-06T00:00:00Z"),s=new Date("2025-12-05T05:00:00Z"),u="";o<s&&(u=`
\u{1F680} \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u{1F680} \u{1F680} \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u{1F680}
@@ -27,19 +33,19 @@ This message was not added to your startup context, so you can continue working
\u2B50 Your upvote means the world - thank you! \u2B50 Your upvote means the world - thank you!
\u{1F680} \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u{1F680} \u{1F680} \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u{1F680}
`);let _="";if(o<a){let f=o.getUTCHours()*60+o.getUTCMinutes(),p=Math.floor((f-300+1440)%1440/60),m=o.getUTCDate(),A=o.getUTCMonth(),C=o.getUTCFullYear()===2025&&A===11&&m>=1&&m<=5,D=p>=17&&p<19;C&&D?_=` `);let T="";if(o<r){let d=o.getUTCHours()*60+o.getUTCMinutes(),l=Math.floor((d-300+1440)%1440/60),O=o.getUTCDate(),g=o.getUTCMonth(),N=o.getUTCFullYear()===2025&&g===11&&O>=1&&O<=5,D=l>=17&&l<19;N&&D?T=`
\u{1F534} LIVE NOW: AMA w/ Dev (@thedotmack) until 7pm EST \u{1F534} LIVE NOW: AMA w/ Dev (@thedotmack) until 7pm EST
`:_=` `:T=`
\u2013 LIVE AMA w/ Dev (@thedotmack) Dec 1st\u20135th, 5pm to 7pm EST \u2013 LIVE AMA w/ Dev (@thedotmack) Dec 1st\u20135th, 5pm to 7pm EST
`}console.error(` `}console.error(`
\u{1F4DD} Claude-Mem Context Loaded \u{1F4DD} Claude-Mem Context Loaded
\u2139\uFE0F Note: This appears as stderr but is informational only \u2139\uFE0F Note: This appears as stderr but is informational only
`+u+` `+i+`
\u{1F4A1} New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history. \u{1F4A1} New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.
\u{1F4AC} Community https://discord.gg/J4wttp9vDu`+T+_+` \u{1F4AC} Community https://discord.gg/J4wttp9vDu`+u+T+`
\u{1F4FA} Watch live in browser http://localhost:${n}/ \u{1F4FA} Watch live in browser http://localhost:${e}/
`)}catch(n){console.error(`\u274C Failed to load context display: ${n}`)}process.exit(3); `)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
File diff suppressed because one or more lines are too long
+11 -5
View File
@@ -15,6 +15,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
import { execSync, spawnSync } from 'child_process'; import { execSync, spawnSync } from 'child_process';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { homedir } from 'os';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -26,6 +27,10 @@ const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version');
const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules'); const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules');
const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3'); const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3');
// CRITICAL: Always use marketplace directory for PM2/ecosystem
// This ensures cross-platform compatibility and avoids cache directory confusion
const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Colors for output // Colors for output
const colors = { const colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
@@ -367,15 +372,16 @@ async function main() {
// Try to start the PM2 worker after fresh install // Try to start the PM2 worker after fresh install
try { try {
log('🚀 Starting worker service...', colors.cyan); log('🚀 Starting worker service...', colors.cyan);
// On Windows, PM2 executable is pm2.cmd, not pm2 // CRITICAL: Always use marketplace directory for PM2/ecosystem
const localPm2Base = join(NODE_MODULES_PATH, '.bin', 'pm2'); // This ensures PM2 starts from the correct location regardless of where this script runs from
const localPm2Base = join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base; const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base;
const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2'; const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2';
const ecosystemPath = join(PLUGIN_ROOT, 'ecosystem.config.cjs'); const ecosystemPath = join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
// Using spawnSync with array args to avoid command injection risks // Using spawnSync with array args to avoid command injection risks
const result = spawnSync(pm2Command, ['start', ecosystemPath], { const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: PLUGIN_ROOT, cwd: MARKETPLACE_ROOT,
stdio: 'pipe', stdio: 'pipe',
encoding: 'utf-8' encoding: 'utf-8'
}); });
@@ -387,7 +393,7 @@ async function main() {
} catch (error) { } catch (error) {
// Worker might already be running or PM2 not available - that's okay // Worker might already be running or PM2 not available - that's okay
// The ensureWorkerRunning() function will handle auto-start when needed // The ensureWorkerRunning() function will handle auto-start when needed
log('️ Worker startup error', colors.dim); log(' Worker will start automatically when needed', colors.dim);
} }
// Success - dependencies installed (if needed) // Success - dependencies installed (if needed)
+26 -2
View File
@@ -7,11 +7,12 @@
*/ */
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { existsSync } = require('fs'); const { existsSync, readFileSync } = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
const INSTALLED_PATH = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); const INSTALLED_PATH = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const CACHE_BASE_PATH = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'thedotmack', 'claude-mem');
function getCurrentBranch() { function getCurrentBranch() {
try { try {
@@ -29,8 +30,9 @@ function getCurrentBranch() {
} }
const branch = getCurrentBranch(); const branch = getCurrentBranch();
const isForce = process.argv.includes('--force');
if (branch && branch !== 'main') { if (branch && branch !== 'main' && !isForce) {
console.log(''); console.log('');
console.log('\x1b[33m%s\x1b[0m', `WARNING: Installed plugin is on beta branch: ${branch}`); console.log('\x1b[33m%s\x1b[0m', `WARNING: Installed plugin is on beta branch: ${branch}`);
console.log('\x1b[33m%s\x1b[0m', 'Running rsync would overwrite beta code.'); console.log('\x1b[33m%s\x1b[0m', 'Running rsync would overwrite beta code.');
@@ -43,6 +45,18 @@ if (branch && branch !== 'main') {
process.exit(1); process.exit(1);
} }
// Get version from plugin.json
function getPluginVersion() {
try {
const pluginJsonPath = path.join(__dirname, '..', 'plugin', '.claude-plugin', 'plugin.json');
const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
return pluginJson.version;
} catch (error) {
console.error('\x1b[31m%s\x1b[0m', 'Failed to read plugin version:', error.message);
process.exit(1);
}
}
// Normal rsync for main branch or fresh install // Normal rsync for main branch or fresh install
console.log('Syncing to marketplace...'); console.log('Syncing to marketplace...');
try { try {
@@ -57,6 +71,16 @@ try {
{ stdio: 'inherit' } { stdio: 'inherit' }
); );
// Sync to cache folder with version
const version = getPluginVersion();
const CACHE_VERSION_PATH = path.join(CACHE_BASE_PATH, version);
console.log(`Syncing to cache folder (version ${version})...`);
execSync(
`rsync -av --delete --exclude=.git plugin/ "${CACHE_VERSION_PATH}/"`,
{ stdio: 'inherit' }
);
console.log('\x1b[32m%s\x1b[0m', 'Sync complete!'); console.log('\x1b[32m%s\x1b[0m', 'Sync complete!');
} catch (error) { } catch (error) {
console.error('\x1b[31m%s\x1b[0m', 'Sync failed:', error.message); console.error('\x1b[31m%s\x1b[0m', 'Sync failed:', error.message);
+4 -1
View File
@@ -7,7 +7,7 @@
*/ */
import { stdin } from 'process'; import { stdin } from 'process';
import { getWorkerPort } from '../shared/worker-utils.js'; import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js'; import { silentDebug } from '../utils/silent-debug.js';
export interface SessionEndInput { export interface SessionEndInput {
@@ -22,6 +22,9 @@ export interface SessionEndInput {
* Cleanup Hook Main Logic - Fire-and-forget HTTP client * Cleanup Hook Main Logic - Fire-and-forget HTTP client
*/ */
async function cleanupHook(input?: SessionEndInput): Promise<void> { async function cleanupHook(input?: SessionEndInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
silentDebug('[cleanup-hook] Hook fired', { silentDebug('[cleanup-hook] Hook fired', {
session_id: input?.session_id, session_id: input?.session_id,
cwd: input?.cwd, cwd: input?.cwd,
+4 -26
View File
@@ -9,7 +9,7 @@
import path from "path"; import path from "path";
import { stdin } from "process"; import { stdin } from "process";
import { execSync } from "child_process"; import { execSync } from "child_process";
import { getWorkerPort } from "../shared/worker-utils.js"; import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
export interface SessionStartInput { export interface SessionStartInput {
session_id?: string; session_id?: string;
@@ -20,36 +20,14 @@ export interface SessionStartInput {
[key: string]: any; [key: string]: any;
} }
async function waitForPort(port: number, maxWaitMs: number = 10000): Promise<boolean> {
const startTime = Date.now();
const pollInterval = 100;
while (Date.now() - startTime < maxWaitMs) {
try {
execSync(`curl -s -f -m 1 "http://127.0.0.1:${port}/api/health" > /dev/null 2>&1`, {
timeout: 1000,
});
return true;
} catch {
await new Promise((resolve) => setTimeout(resolve, pollInterval));
}
}
return false;
}
async function contextHook(input?: SessionStartInput): Promise<string> { async function contextHook(input?: SessionStartInput): Promise<string> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const cwd = input?.cwd ?? process.cwd(); const cwd = input?.cwd ?? process.cwd();
const project = cwd ? path.basename(cwd) : "unknown-project"; const project = cwd ? path.basename(cwd) : "unknown-project";
const port = getWorkerPort(); const port = getWorkerPort();
// Wait for worker to be available
const isAvailable = await waitForPort(port);
if (!isAvailable) {
throw new Error(
`Worker service not available on port ${port} after 10s. Try: npm run worker:restart`
);
}
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`; const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 }); const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
return result.trim(); return result.trim();
+3 -3
View File
@@ -53,6 +53,9 @@ export interface UserPromptSubmitInput {
* New Hook Main Logic * New Hook Main Logic
*/ */
async function newHook(input?: UserPromptSubmitInput): Promise<void> { async function newHook(input?: UserPromptSubmitInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
if (!input) { if (!input) {
throw new Error('newHook requires input'); throw new Error('newHook requires input');
} }
@@ -79,9 +82,6 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
cwd_was: cwd cwd_was: cwd
}); });
// Ensure worker is running
await ensureWorkerRunning();
const db = new SessionStore(); const db = new SessionStore();
// CRITICAL: Use session_id from hook as THE source of truth // CRITICAL: Use session_id from hook as THE source of truth
+3 -3
View File
@@ -33,6 +33,9 @@ const SKIP_TOOLS = new Set([
* Save Hook Main Logic - Fire-and-forget HTTP client * Save Hook Main Logic - Fire-and-forget HTTP client
*/ */
async function saveHook(input?: PostToolUseInput): Promise<void> { async function saveHook(input?: PostToolUseInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
if (!input) { if (!input) {
throw new Error('saveHook requires input'); throw new Error('saveHook requires input');
} }
@@ -44,9 +47,6 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
return; return;
} }
// Ensure worker is running
await ensureWorkerRunning();
const port = getWorkerPort(); const port = getWorkerPort();
const toolStr = logger.formatTool(tool_name, tool_input); const toolStr = logger.formatTool(tool_name, tool_input);
+3 -3
View File
@@ -130,15 +130,15 @@ function extractLastAssistantMessage(transcriptPath: string): string {
* Summary Hook Main Logic - Fire-and-forget HTTP client * Summary Hook Main Logic - Fire-and-forget HTTP client
*/ */
async function summaryHook(input?: StopInput): Promise<void> { async function summaryHook(input?: StopInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
if (!input) { if (!input) {
throw new Error('summaryHook requires input'); throw new Error('summaryHook requires input');
} }
const { session_id } = input; const { session_id } = input;
// Ensure worker is running
await ensureWorkerRunning();
const port = getWorkerPort(); const port = getWorkerPort();
// Extract last user AND assistant messages from transcript // Extract last user AND assistant messages from transcript
+4 -1
View File
@@ -9,7 +9,7 @@
import { join, basename } from "path"; import { join, basename } from "path";
import { homedir } from "os"; import { homedir } from "os";
import { existsSync } from "fs"; import { existsSync } from "fs";
import { getWorkerPort } from "../shared/worker-utils.js"; import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
// Check if node_modules exists - if not, this is first run // Check if node_modules exists - if not, this is first run
const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
@@ -40,6 +40,9 @@ This message was not added to your startup context, so you can continue working
} }
try { try {
// Ensure worker is running
await ensureWorkerRunning();
const port = getWorkerPort(); const port = getWorkerPort();
const project = basename(process.cwd()); const project = basename(process.cwd());
+10 -10
View File
@@ -49,7 +49,7 @@ async function callWorkerAPI(
endpoint: string, endpoint: string,
params: Record<string, any> params: Record<string, any>
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> { ): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
silentDebug('[search-server] → Worker API', { endpoint, params }); silentDebug('[mcp-server] → Worker API', { endpoint, params });
try { try {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
@@ -71,12 +71,12 @@ async function callWorkerAPI(
const data = await response.json() as { content: Array<{ type: 'text'; text: string }>; isError?: boolean }; const data = await response.json() as { content: Array<{ type: 'text'; text: string }>; isError?: boolean };
silentDebug('[search-server] ← Worker API success', { endpoint }); silentDebug('[mcp-server] ← Worker API success', { endpoint });
// Worker returns { content: [...] } format directly // Worker returns { content: [...] } format directly
return data; return data;
} catch (error: any) { } catch (error: any) {
silentDebug('[search-server] ← Worker API error', { endpoint, error: error.message }); silentDebug('[mcp-server] ← Worker API error', { endpoint, error: error.message });
return { return {
content: [{ content: [{
type: 'text' as const, type: 'text' as const,
@@ -411,7 +411,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Cleanup function // Cleanup function
async function cleanup() { async function cleanup() {
silentDebug('[search-server] Shutting down...'); silentDebug('[mcp-server] Shutting down...');
process.exit(0); process.exit(0);
} }
@@ -424,22 +424,22 @@ async function main() {
// Start the MCP server // Start the MCP server
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); await server.connect(transport);
silentDebug('[search-server] Claude-mem search server started'); silentDebug('[mcp-server] Claude-mem search server started');
// Check Worker availability in background // Check Worker availability in background
setTimeout(async () => { setTimeout(async () => {
const workerAvailable = await verifyWorkerConnection(); const workerAvailable = await verifyWorkerConnection();
if (!workerAvailable) { if (!workerAvailable) {
silentDebug('[search-server] WARNING: Worker not available at', WORKER_BASE_URL); silentDebug('[mcp-server] WARNING: Worker not available at', WORKER_BASE_URL);
silentDebug('[search-server] Tools will fail until Worker is started'); silentDebug('[mcp-server] Tools will fail until Worker is started');
silentDebug('[search-server] Start Worker with: npm run worker:restart'); silentDebug('[mcp-server] Start Worker with: npm run worker:restart');
} else { } else {
silentDebug('[search-server] Worker available at', WORKER_BASE_URL); silentDebug('[mcp-server] Worker available at', WORKER_BASE_URL);
} }
}, 0); }, 0);
} }
main().catch((error) => { main().catch((error) => {
silentDebug('[search-server] Fatal error:', error); silentDebug('[mcp-server] Fatal error:', error);
process.exit(1); process.exit(1);
}); });
+58 -58
View File
@@ -97,7 +97,7 @@ export class SearchManager {
// PATH 1: FILTER-ONLY (no query text) - Skip Chroma/FTS5, use direct SQLite filtering // 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) // This path enables date filtering which Chroma cannot do (requires direct SQLite access)
if (!query) { if (!query) {
silentDebug(`[search-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`); silentDebug(`[mcp-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
const obsOptions = { ...options, type: obs_type, concepts, files }; const obsOptions = { ...options, type: obs_type, concepts, files };
if (searchObservations) { if (searchObservations) {
observations = this.sessionSearch.searchObservations(undefined, obsOptions); observations = this.sessionSearch.searchObservations(undefined, obsOptions);
@@ -113,7 +113,7 @@ export class SearchManager {
else if (this.chromaSync) { else if (this.chromaSync) {
let chromaSucceeded = false; let chromaSucceeded = false;
try { try {
silentDebug(`[search-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`); silentDebug(`[mcp-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
// Build Chroma where filter for doc_type // Build Chroma where filter for doc_type
let whereFilter: Record<string, any> | undefined; let whereFilter: Record<string, any> | undefined;
@@ -128,7 +128,7 @@ export class SearchManager {
// Step 1: Chroma semantic search with optional type filter // Step 1: Chroma semantic search with optional type filter
const chromaResults = await this.queryChroma(query, 100, whereFilter); const chromaResults = await this.queryChroma(query, 100, whereFilter);
chromaSucceeded = true; // Chroma didn't throw error chromaSucceeded = true; // Chroma didn't throw error
silentDebug(`[search-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`); silentDebug(`[mcp-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) { if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days) // Step 2: Filter by recency (90 days)
@@ -139,7 +139,7 @@ export class SearchManager {
isRecent: meta && meta.created_at_epoch > ninetyDaysAgo isRecent: meta && meta.created_at_epoch > ninetyDaysAgo
})).filter(item => item.isRecent); })).filter(item => item.isRecent);
silentDebug(`[search-server] ${recentMetadata.length} results within 90-day window`); silentDebug(`[mcp-server] ${recentMetadata.length} results within 90-day window`);
// Step 3: Categorize IDs by document type // Step 3: Categorize IDs by document type
const obsIds: number[] = []; const obsIds: number[] = [];
@@ -157,7 +157,7 @@ export class SearchManager {
} }
} }
silentDebug(`[search-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`); silentDebug(`[mcp-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
// Step 4: Hydrate from SQLite with additional filters // Step 4: Hydrate from SQLite with additional filters
if (obsIds.length > 0) { if (obsIds.length > 0) {
@@ -172,14 +172,14 @@ export class SearchManager {
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit }); prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit });
} }
silentDebug(`[search-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`); silentDebug(`[mcp-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
} else { } else {
// Chroma returned 0 results - this is the correct answer, don't fall back to FTS5 // 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)`); silentDebug(`[mcp-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message); silentDebug('[mcp-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/'); silentDebug('[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
// Return empty results - no fallback // Return empty results - no fallback
observations = []; observations = [];
sessions = []; sessions = [];
@@ -188,8 +188,8 @@ export class SearchManager {
} }
// ChromaDB not initialized - return empty results (no fallback) // ChromaDB not initialized - return empty results (no fallback)
else { else {
silentDebug(`[search-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`); silentDebug(`[mcp-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/`); silentDebug(`[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
observations = []; observations = [];
sessions = []; sessions = [];
prompts = []; prompts = [];
@@ -312,9 +312,9 @@ export class SearchManager {
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using hybrid semantic search for timeline query'); silentDebug('[mcp-server] Using hybrid semantic search for timeline query');
const chromaResults = await this.queryChroma(query, 100); const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[search-server] Chroma returned ${chromaResults?.ids?.length ?? 0} semantic matches`); silentDebug(`[mcp-server] Chroma returned ${chromaResults?.ids?.length ?? 0} semantic matches`);
if (chromaResults?.ids && chromaResults.ids.length > 0) { if (chromaResults?.ids && chromaResults.ids.length > 0) {
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000); const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
@@ -328,7 +328,7 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message); silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
} }
} }
@@ -345,7 +345,7 @@ export class SearchManager {
const topResult = results[0]; const topResult = results[0];
anchorId = topResult.id; anchorId = topResult.id;
anchorEpoch = topResult.created_at_epoch; anchorEpoch = topResult.created_at_epoch;
silentDebug(`[search-server] Query mode: Using observation #${topResult.id} as timeline anchor`); silentDebug(`[mcp-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
timelineData = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project); timelineData = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project);
} }
// MODE 2: Anchor-based timeline // MODE 2: Anchor-based timeline
@@ -621,7 +621,7 @@ export class SearchManager {
try { try {
if (query) { if (query) {
// Semantic search filtered to decision type // Semantic search filtered to decision type
silentDebug('[search-server] Using Chroma semantic search with type=decision filter'); silentDebug('[mcp-server] Using Chroma semantic search with type=decision filter');
const chromaResults = await this.queryChroma(query, Math.min((filters.limit || 20) * 2, 100), { type: 'decision' }); const chromaResults = await this.queryChroma(query, Math.min((filters.limit || 20) * 2, 100), { type: 'decision' });
const obsIds = chromaResults.ids; const obsIds = chromaResults.ids;
@@ -632,7 +632,7 @@ export class SearchManager {
} }
} else { } else {
// No query: get all decisions, rank by "decision" keyword // No query: get all decisions, rank by "decision" keyword
silentDebug('[search-server] Using metadata-first + semantic ranking for decisions'); silentDebug('[mcp-server] Using metadata-first + semantic ranking for decisions');
const metadataResults = this.sessionSearch.findByType('decision', filters); const metadataResults = this.sessionSearch.findByType('decision', filters);
if (metadataResults.length > 0) { if (metadataResults.length > 0) {
@@ -653,7 +653,7 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma search failed, using SQLite fallback:', chromaError.message); silentDebug('[mcp-server] Chroma search failed, using SQLite fallback:', chromaError.message);
} }
} }
@@ -709,7 +709,7 @@ export class SearchManager {
// Search for change-type observations and change-related concepts // Search for change-type observations and change-related concepts
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using hybrid search for change-related observations'); silentDebug('[mcp-server] Using hybrid search for change-related observations');
// Get all observations with type="change" or concepts containing change // Get all observations with type="change" or concepts containing change
const typeResults = this.sessionSearch.findByType('change', filters); const typeResults = this.sessionSearch.findByType('change', filters);
@@ -737,7 +737,7 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message); silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
} }
} }
@@ -807,7 +807,7 @@ export class SearchManager {
// Search for how-it-works concept observations // Search for how-it-works concept observations
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using metadata-first + semantic ranking for how-it-works'); silentDebug('[mcp-server] Using metadata-first + semantic ranking for how-it-works');
const metadataResults = this.sessionSearch.findByConcept('how-it-works', filters); const metadataResults = this.sessionSearch.findByConcept('how-it-works', filters);
if (metadataResults.length > 0) { if (metadataResults.length > 0) {
@@ -827,7 +827,7 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message); silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
} }
} }
@@ -883,11 +883,11 @@ export class SearchManager {
// Vector-first search via ChromaDB // Vector-first search via ChromaDB
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using hybrid semantic search (Chroma + SQLite)'); silentDebug('[mcp-server] Using hybrid semantic search (Chroma + SQLite)');
// Step 1: Chroma semantic search (top 100) // Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100); const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`); silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) { if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days) // Step 2: Filter by recency (90 days)
@@ -897,17 +897,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo; return meta && meta.created_at_epoch > ninetyDaysAgo;
}); });
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`); silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order // Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) { if (recentIds.length > 0) {
const limit = options.limit || 20; const limit = options.limit || 20;
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit }); results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[search-server] Hydrated ${results.length} observations from SQLite`); silentDebug(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message); silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
} }
} }
@@ -960,11 +960,11 @@ export class SearchManager {
// Vector-first search via ChromaDB // Vector-first search via ChromaDB
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using hybrid semantic search for sessions'); silentDebug('[mcp-server] Using hybrid semantic search for sessions');
// Step 1: Chroma semantic search (top 100) // Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'session_summary' }); const chromaResults = await this.queryChroma(query, 100, { doc_type: 'session_summary' });
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`); silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) { if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days) // Step 2: Filter by recency (90 days)
@@ -974,17 +974,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo; return meta && meta.created_at_epoch > ninetyDaysAgo;
}); });
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`); silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order // Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) { if (recentIds.length > 0) {
const limit = options.limit || 20; const limit = options.limit || 20;
results = this.sessionStore.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit }); results = this.sessionStore.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[search-server] Hydrated ${results.length} sessions from SQLite`); silentDebug(`[mcp-server] Hydrated ${results.length} sessions from SQLite`);
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message); silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
} }
} }
@@ -1037,11 +1037,11 @@ export class SearchManager {
// Vector-first search via ChromaDB // Vector-first search via ChromaDB
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using hybrid semantic search for user prompts'); silentDebug('[mcp-server] Using hybrid semantic search for user prompts');
// Step 1: Chroma semantic search (top 100) // Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'user_prompt' }); const chromaResults = await this.queryChroma(query, 100, { doc_type: 'user_prompt' });
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`); silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) { if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days) // Step 2: Filter by recency (90 days)
@@ -1051,17 +1051,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo; return meta && meta.created_at_epoch > ninetyDaysAgo;
}); });
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`); silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order // Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) { if (recentIds.length > 0) {
const limit = options.limit || 20; const limit = options.limit || 20;
results = this.sessionStore.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit }); results = this.sessionStore.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[search-server] Hydrated ${results.length} user prompts from SQLite`); silentDebug(`[mcp-server] Hydrated ${results.length} user prompts from SQLite`);
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message); silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
} }
} }
@@ -1114,11 +1114,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search // Metadata-first, semantic-enhanced search
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using metadata-first + semantic ranking for concept search'); silentDebug('[mcp-server] Using metadata-first + semantic ranking for concept search');
// Step 1: SQLite metadata filter (get all IDs with this concept) // Step 1: SQLite metadata filter (get all IDs with this concept)
const metadataResults = this.sessionSearch.findByConcept(concept, filters); const metadataResults = this.sessionSearch.findByConcept(concept, filters);
silentDebug(`[search-server] Found ${metadataResults.length} observations with concept "${concept}"`); silentDebug(`[mcp-server] Found ${metadataResults.length} observations with concept "${concept}"`);
if (metadataResults.length > 0) { if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to concept) // Step 2: Chroma semantic ranking (rank by relevance to concept)
@@ -1133,7 +1133,7 @@ export class SearchManager {
} }
} }
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`); silentDebug(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
// Step 3: Hydrate in semantic rank order // Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) { if (rankedIds.length > 0) {
@@ -1143,14 +1143,14 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message); silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback // Fall through to SQLite fallback
} }
} }
// Fall back to SQLite-only if Chroma unavailable or failed // Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) { if (results.length === 0) {
silentDebug('[search-server] Using SQLite-only concept search'); silentDebug('[mcp-server] Using SQLite-only concept search');
results = this.sessionSearch.findByConcept(concept, filters); results = this.sessionSearch.findByConcept(concept, filters);
} }
@@ -1204,11 +1204,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search for observations // Metadata-first, semantic-enhanced search for observations
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using metadata-first + semantic ranking for file search'); silentDebug('[mcp-server] Using metadata-first + semantic ranking for file search');
// Step 1: SQLite metadata filter (get all results with this file) // Step 1: SQLite metadata filter (get all results with this file)
const metadataResults = this.sessionSearch.findByFile(filePath, filters); const metadataResults = this.sessionSearch.findByFile(filePath, filters);
silentDebug(`[search-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`); silentDebug(`[mcp-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`);
// Sessions: Keep as-is (already summarized, no semantic ranking needed) // Sessions: Keep as-is (already summarized, no semantic ranking needed)
sessions = metadataResults.sessions; sessions = metadataResults.sessions;
@@ -1227,7 +1227,7 @@ export class SearchManager {
} }
} }
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`); silentDebug(`[mcp-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
// Step 3: Hydrate in semantic rank order // Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) { if (rankedIds.length > 0) {
@@ -1237,14 +1237,14 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message); silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback // Fall through to SQLite fallback
} }
} }
// Fall back to SQLite-only if Chroma unavailable or failed // Fall back to SQLite-only if Chroma unavailable or failed
if (observations.length === 0 && sessions.length === 0) { if (observations.length === 0 && sessions.length === 0) {
silentDebug('[search-server] Using SQLite-only file search'); silentDebug('[mcp-server] Using SQLite-only file search');
const results = this.sessionSearch.findByFile(filePath, filters); const results = this.sessionSearch.findByFile(filePath, filters);
observations = results.observations; observations = results.observations;
sessions = results.sessions; sessions = results.sessions;
@@ -1323,11 +1323,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search // Metadata-first, semantic-enhanced search
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using metadata-first + semantic ranking for type search'); silentDebug('[mcp-server] Using metadata-first + semantic ranking for type search');
// Step 1: SQLite metadata filter (get all IDs with this type) // Step 1: SQLite metadata filter (get all IDs with this type)
const metadataResults = this.sessionSearch.findByType(type, filters); const metadataResults = this.sessionSearch.findByType(type, filters);
silentDebug(`[search-server] Found ${metadataResults.length} observations with type "${typeStr}"`); silentDebug(`[mcp-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
if (metadataResults.length > 0) { if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to type) // Step 2: Chroma semantic ranking (rank by relevance to type)
@@ -1342,7 +1342,7 @@ export class SearchManager {
} }
} }
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`); silentDebug(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
// Step 3: Hydrate in semantic rank order // Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) { if (rankedIds.length > 0) {
@@ -1352,14 +1352,14 @@ export class SearchManager {
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message); silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback // Fall through to SQLite fallback
} }
} }
// Fall back to SQLite-only if Chroma unavailable or failed // Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) { if (results.length === 0) {
silentDebug('[search-server] Using SQLite-only type search'); silentDebug('[mcp-server] Using SQLite-only type search');
results = this.sessionSearch.findByType(type, filters); results = this.sessionSearch.findByType(type, filters);
} }
@@ -1815,9 +1815,9 @@ export class SearchManager {
// Use hybrid search if available // Use hybrid search if available
if (this.chromaSync) { if (this.chromaSync) {
try { try {
silentDebug('[search-server] Using hybrid semantic search for timeline query'); silentDebug('[mcp-server] Using hybrid semantic search for timeline query');
const chromaResults = await this.queryChroma(query, 100); const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`); silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) { if (chromaResults.ids.length > 0) {
// Filter by recency (90 days) // Filter by recency (90 days)
@@ -1827,15 +1827,15 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo; return meta && meta.created_at_epoch > ninetyDaysAgo;
}); });
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`); silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
if (recentIds.length > 0) { if (recentIds.length > 0) {
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit: mode === 'auto' ? 1 : limit }); results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit: mode === 'auto' ? 1 : limit });
silentDebug(`[search-server] Hydrated ${results.length} observations from SQLite`); silentDebug(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
} }
} }
} catch (chromaError: any) { } catch (chromaError: any) {
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message); silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
} }
} }
@@ -1886,7 +1886,7 @@ export class SearchManager {
} else { } else {
// Auto mode: Use top result as timeline anchor // Auto mode: Use top result as timeline anchor
const topResult = results[0]; const topResult = results[0];
silentDebug(`[search-server] Auto mode: Using observation #${topResult.id} as timeline anchor`); silentDebug(`[mcp-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
// Get timeline around this observation // Get timeline around this observation
const timelineData = this.sessionStore.getTimelineAroundObservation( const timelineData = this.sessionStore.getTimelineAroundObservation(
+12 -9
View File
@@ -1,9 +1,13 @@
import path from "path"; import path from "path";
import { existsSync } from "fs";
import { homedir } from "os"; import { homedir } from "os";
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import { getPackageRoot } from "./paths.js";
import { SettingsDefaultsManager } from "../services/worker/settings/SettingsDefaultsManager.js"; import { SettingsDefaultsManager } from "../services/worker/settings/SettingsDefaultsManager.js";
// CRITICAL: Always use marketplace directory for PM2/ecosystem
// This ensures cross-platform compatibility and avoids cache directory confusion
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Named constants for health checks // Named constants for health checks
const HEALTH_CHECK_TIMEOUT_MS = 100; const HEALTH_CHECK_TIMEOUT_MS = 100;
const WORKER_STARTUP_WAIT_MS = 500; const WORKER_STARTUP_WAIT_MS = 500;
@@ -39,9 +43,9 @@ async function isWorkerHealthy(): Promise<boolean> {
*/ */
async function startWorker(): Promise<boolean> { async function startWorker(): Promise<boolean> {
try { try {
// Find the ecosystem config file (built version in plugin/) // CRITICAL: Always use marketplace directory for ecosystem.config.cjs
const pluginRoot = getPackageRoot(); // This ensures PM2 starts from the correct location regardless of where hooks run from
const ecosystemPath = path.join(pluginRoot, 'ecosystem.config.cjs'); const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
if (!existsSync(ecosystemPath)) { if (!existsSync(ecosystemPath)) {
throw new Error(`Ecosystem config not found at ${ecosystemPath}`); throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
@@ -49,15 +53,15 @@ async function startWorker(): Promise<boolean> {
// Try to use local PM2 from node_modules first, fall back to global PM2 // Try to use local PM2 from node_modules first, fall back to global PM2
// On Windows, PM2 executable is pm2.cmd, not pm2 // On Windows, PM2 executable is pm2.cmd, not pm2
const localPm2Base = path.join(pluginRoot, 'node_modules', '.bin', 'pm2'); const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base; const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base;
const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2'; const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2';
// Start using PM2 with the ecosystem config // Start using PM2 with the ecosystem config
// CRITICAL: Must set cwd to pluginRoot so PM2 starts from marketplace directory // CRITICAL: Must set cwd to MARKETPLACE_ROOT so PM2 starts from marketplace directory
// Using spawnSync with array args to avoid command injection risks // Using spawnSync with array args to avoid command injection risks
const result = spawnSync(pm2Command, ['start', ecosystemPath], { const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: pluginRoot, cwd: MARKETPLACE_ROOT,
stdio: 'pipe', stdio: 'pipe',
encoding: 'utf-8', encoding: 'utf-8',
windowsHide: true windowsHide: true
@@ -96,11 +100,10 @@ export async function ensureWorkerRunning(): Promise<void> {
if (!started) { if (!started) {
const port = getWorkerPort(); const port = getWorkerPort();
const pluginRoot = getPackageRoot();
throw new Error( throw new Error(
`Worker service failed to start on port ${port}.\n\n` + `Worker service failed to start on port ${port}.\n\n` +
`To start manually, run:\n` + `To start manually, run:\n` +
` cd ${pluginRoot}\n` + ` cd ${MARKETPLACE_ROOT}\n` +
` npx pm2 start ecosystem.config.cjs\n\n` + ` npx pm2 start ecosystem.config.cjs\n\n` +
`If already running, try: npx pm2 restart claude-mem-worker` `If already running, try: npx pm2 restart claude-mem-worker`
); );