Merge pull request #201 from thedotmack/bugfix/settings-and-new-hook

Settings centralization and new-hook HTTP refactor
This commit is contained in:
Alex Newman
2025-12-09 16:25:58 -05:00
committed by GitHub
39 changed files with 1393 additions and 870 deletions
+17 -1
View File
@@ -42,12 +42,28 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
**Viewer UI**: `npm run build && npm run sync-marketplace && npm run worker:restart`
## Environment Variables
## Configuration
Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created with defaults on first run.
**Core Settings:**
- `CLAUDE_MEM_MODEL` - Model for observations/summaries (default: claude-haiku-4-5)
- `CLAUDE_MEM_CONTEXT_OBSERVATIONS` - Observations injected at SessionStart (default: 50)
- `CLAUDE_MEM_WORKER_PORT` - Worker service port (default: 37777)
**System Configuration:**
- `CLAUDE_MEM_DATA_DIR` - Data directory location (default: ~/.claude-mem)
- `CLAUDE_MEM_LOG_LEVEL` - Log verbosity: DEBUG, INFO, WARN, ERROR, SILENT (default: INFO)
- `CLAUDE_MEM_PYTHON_VERSION` - Python version for uvx/chroma-mcp (default: 3.13, avoids onnxruntime compatibility issues with Python 3.14+)
- `CLAUDE_CODE_PATH` - Path to Claude executable (default: auto-detect via 'which claude')
**Settings File Format:**
```json
{
"CLAUDE_MEM_MODEL": "claude-haiku-4-5",
"CLAUDE_MEM_WORKER_PORT": "37777"
}
```
## File Locations
+30 -6
View File
@@ -306,18 +306,42 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
## Configuration
**Model Selection:**
Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created with defaults on first run.
**Available Settings:**
| Setting | Default | Description |
|---------|---------|-------------|
| `CLAUDE_MEM_MODEL` | `claude-haiku-4-5` | AI model for observations |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem` | Data directory location |
| `CLAUDE_MEM_LOG_LEVEL` | `INFO` | Log verbosity (DEBUG, INFO, WARN, ERROR, SILENT) |
| `CLAUDE_MEM_PYTHON_VERSION` | `3.13` | Python version for chroma-mcp |
| `CLAUDE_CODE_PATH` | _(auto-detect)_ | Path to Claude executable |
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject at SessionStart |
**Settings Management:**
```bash
# Edit settings via CLI helper
./claude-mem-settings.sh
# Or edit directly
nano ~/.claude-mem/settings.json
# View current settings
curl http://localhost:37777/api/settings
```
**Environment Variables:**
**Settings File Format:**
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-haiku-4-5)
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
- `CLAUDE_MEM_PYTHON_VERSION` - Python version for uvx/chroma-mcp (default: 3.13)
```json
{
"CLAUDE_MEM_MODEL": "claude-haiku-4-5",
"CLAUDE_MEM_WORKER_PORT": "37777",
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "50"
}
```
See [Configuration Guide](https://docs.claude-mem.ai/configuration) for details.
+561
View File
@@ -0,0 +1,561 @@
# Claude-Mem Smart Install & Plugin Hooks - Comprehensive Analysis
**Generated:** 2025-12-09
**Scope:** Smart install system, all plugin hooks, cross-platform compatibility, error handling, edge cases
---
## Executive Summary
This report provides a comprehensive analysis of claude-mem's smart install system and plugin hook infrastructure. The analysis focuses on cross-platform compatibility, error handling patterns, artificial blockers, and edge case handling.
**Key Findings:**
- ✅ Overall architecture is well-designed with clear separation of concerns
- ⚠️ Multiple cross-platform compatibility issues identified
- ⚠️ Several silent failure patterns that hinder debugging
- ⚠️ Artificial blockers that could prevent legitimate use cases
- ⚠️ Inconsistent timeout values across different components
- ✅ No nested try-catch anti-patterns found
---
## Architecture Overview
### Smart Install System Flow
```
User Invokes Hook
ensureWorkerRunning() [worker-utils.ts]
isWorkerHealthy() → fetch /health endpoint
├─ [HEALTHY] → Continue
└─ [UNHEALTHY] → startWorker()
├─ [Windows] → PowerShell Start-Process (hidden window)
└─ [Unix] → PM2 start ecosystem.config.cjs
Wait for health check (15 retries × 1000ms)
├─ [SUCCESS] → Continue
└─ [FAILURE] → Throw error with manual recovery instructions
```
### Plugin Hook Lifecycle
1. **SessionStart** (context-hook.ts + user-message-hook.ts)
- context-hook: Fetches context via HTTP/curl
- user-message-hook: Displays context to user via stderr
2. **UserPromptSubmit** (new-hook.ts)
- Creates/retrieves SDK session
- Strips privacy tags from prompt
- Initializes session via HTTP
3. **PostToolUse** (save-hook.ts)
- Filters skipped tools
- Sends observation to worker via HTTP
4. **Stop** (summary-hook.ts)
- Parses transcript JSONL
- Extracts last user/assistant messages
- Requests summary generation via HTTP
5. **SessionEnd** (cleanup-hook.ts)
- Marks session complete
- Fire-and-forget HTTP request
---
## Cross-Platform Compatibility Issues
### 🔴 CRITICAL: curl Dependency (context-hook.ts)
**Location:** `src/hooks/context-hook.ts:32`
```typescript
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
```
**Issues:**
1. **Windows Compatibility:** curl is not guaranteed to be available on Windows systems (though included in Windows 10 1803+, it may be missing on older systems or custom installations)
2. **Error Handling:** No try-catch around execSync - will throw unhandled exception if curl fails
3. **Redundancy:** Uses curl when JavaScript's native `fetch` is already used everywhere else in the codebase
**Impact:** High - SessionStart hook will crash if curl is unavailable or returns non-zero exit code
**Edge Cases:**
- Corporate proxies blocking curl
- Systems without curl in PATH
- curl returning non-zero exit with valid output (warnings, etc.)
**Recommendation:**
```typescript
// Replace curl with fetch (already used in user-message-hook.ts)
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
const result = await response.text();
```
---
### 🟡 MEDIUM: Platform-Specific Process Spawning (worker-utils.ts)
**Location:** `src/shared/worker-utils.ts:55-93`
**Windows Implementation:**
```typescript
spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process -FilePath 'node' -ArgumentList '${workerScript}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden`
])
```
**Issues:**
1. **PowerShell Dependency:** Assumes PowerShell is available and in PATH
2. **Command Injection Risk:** Worker script path inserted directly into command string without escaping
3. **Process Monitoring:** Windows approach launches detached process with no PM2 monitoring - harder to debug/restart
4. **Health Check Timeout:** Comment says "Windows needs longer timeouts" but timeout is same for all platforms (500ms)
**Edge Cases:**
- Windows systems with PowerShell execution policy restrictions
- Paths containing single quotes or special characters
- Windows subsystem for Linux (WSL) environments
- Wine/Proton compatibility layers
**Unix Implementation:**
```typescript
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
const pm2Command = existsSync(localPm2Base) ? localPm2Base : 'pm2';
```
**Issues:**
1. **PM2 Dependency:** Falls back to global pm2 if local not found, but doesn't verify it exists
2. **Silent Failure:** If PM2 not installed globally, spawnSync will fail with cryptic ENOENT error
**Recommendation:**
- Add pm2 existence check before spawn
- Implement consistent process monitoring across platforms
- Add path escaping for Windows command construction
- Actually implement longer timeout for Windows if needed
---
### 🟡 MEDIUM: Git Dependency (paths.ts)
**Location:** `src/shared/paths.ts:89-97`
```typescript
export function getCurrentProjectName(): string {
try {
const gitRoot = execSync('git rev-parse --show-toplevel', {
cwd: process.cwd(),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return basename(gitRoot);
} catch {
return basename(process.cwd());
}
}
```
**Issues:**
1. **Git Assumption:** Assumes git is installed and available in PATH
2. **Non-Git Projects:** Silently falls back to cwd basename, but this behavior is undocumented
**Edge Cases:**
- Projects not using git
- Monorepos where cwd !== git root is desired
- Systems without git installed
**Status:** ✅ Already handled with fallback, but could benefit from debug logging
---
## Error Handling Analysis
### 🔴 CRITICAL: Silent Failures Without Logging
#### 1. Settings File Loading (early-settings.ts:20-28)
```typescript
try {
if (existsSync(SETTINGS_PATH)) {
const data = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
const fileValue = data.env?.[key];
if (fileValue !== undefined) return fileValue;
}
} catch {
// Fail silently - fall through to env var
}
```
**Problem:**
- Invalid JSON in settings file fails silently
- File read permission errors fail silently
- Users have no way to know their settings file is being ignored
**Impact:** High - Users may think settings are applied when they're actually using defaults
**Recommendation:**
```typescript
} catch (error) {
logger.warn('SETTINGS', 'Failed to load settings file', { path: SETTINGS_PATH }, error);
}
```
---
#### 2. Worker Startup Failure (worker-utils.ts:104-107)
```typescript
try {
// ... worker startup logic ...
} catch (error) {
// Failed to start worker
return false;
}
```
**Problem:**
- Catches ALL errors during worker startup
- Returns boolean with no information about what failed
- User only gets generic error after all retries exhausted
**Impact:** High - Makes debugging worker startup issues extremely difficult
**Recommendation:**
```typescript
} catch (error) {
logger.error('WORKER', 'Failed to start worker', {}, error as Error);
return false;
}
```
---
#### 3. Worker Health Check (worker-utils.ts:30-40)
```typescript
async function isWorkerHealthy(): Promise<boolean> {
try {
const port = getWorkerPort();
const response = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
return response.ok;
} catch {
return false;
}
}
```
**Problem:**
- Network errors, timeouts, and non-200 responses all indistinguishable
- No logging at all - completely silent
**Impact:** Medium - Hard to debug why health checks fail
**Recommendation:**
```typescript
} catch (error) {
logger.debug('WORKER', 'Health check failed', { port }, error);
return false;
}
```
---
#### 4. Tool Formatting (logger.ts:122-124)
```typescript
try {
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
// ...
} catch {
return toolName;
}
```
**Problem:**
- Invalid JSON in tool input fails silently
- Could mask data corruption issues
**Impact:** Low - Only affects log formatting
**Status:** ✅ Acceptable for log formatting, but could log at DEBUG level
---
### 🟢 GOOD: No Nested Try-Catch Anti-Patterns
Analysis confirmed zero instances of nested try-catch blocks. Error handling is consistently at single level per function.
---
## Artificial Blockers & Unnecessary Checks
### 🔴 CRITICAL: First-Run Detection (user-message-hook.ts:14-40)
```typescript
const nodeModulesPath = join(pluginDir, 'node_modules');
if (!existsSync(nodeModulesPath)) {
// Show first-time setup message
console.error(`...`);
process.exit(3);
}
```
**Problems:**
1. **False Positive:** Will trigger if user manually deletes node_modules (e.g., for troubleshooting)
2. **Installation Race:** Could fail if installation is still in progress
3. **Hook-Level Check:** Runs on EVERY SessionStart, not just actual first run
**Impact:** High - Prevents usage until node_modules exists, even if dependencies are installed elsewhere
**Edge Cases:**
- User runs `rm -rf node_modules` for troubleshooting
- Package manager installation interrupted
- Symlinked node_modules (some package managers)
**Recommendation:**
- Use a `.first-run-complete` marker file instead
- Move check to npm postinstall script
- Make check more robust (check for specific required modules)
---
### 🟡 MEDIUM: Overly Specific Validation (paths.ts:117-119)
```typescript
if (!existsSync(join(commandsDir, 'save.md'))) {
throw new Error('Package commands directory missing required files');
}
```
**Problem:**
- Checks for ONE specific file to validate entire directory
- Hardcoded filename could break if files reorganized
- Error message doesn't specify what's missing
**Impact:** Medium - Could prevent package from working after internal refactoring
**Recommendation:**
- Remove check entirely (let actual command invocation fail with better error)
- Or check all required files if validation is critical
---
### 🟡 MEDIUM: Duplicate Health Endpoints
**Locations:**
- `src/services/worker-service.ts:107` - `/api/health`
- `src/services/worker/http/routes/ViewerRoutes.ts:27` - `/health`
**Usage:**
- `worker-utils.ts` uses `/health`
- `mcp-server.ts` uses `/api/health`
**Problem:**
- Redundant endpoints doing the same thing
- Inconsistent usage across codebase
- Maintenance burden
**Impact:** Low - Both work, but creates confusion
**Recommendation:**
- Standardize on `/api/health` (follows REST convention)
- Remove `/health` endpoint
- Update worker-utils.ts to use `/api/health`
---
## Timeout Configuration Issues
### Inconsistent Timeouts Across Components
| Component | Timeout | Location | Purpose |
|-----------|---------|----------|---------|
| Health check | 500ms | worker-utils.ts:13 | Check if worker alive |
| Worker startup wait | 1000ms | worker-utils.ts:14 | Wait between health checks |
| Worker startup retries | 15x | worker-utils.ts:15 | Max retries (15s total) |
| Hook HTTP requests | 2000ms | cleanup-hook.ts:61, save-hook.ts:70, summary-hook.ts:164 | Send data to worker |
| New hook session init | 5000ms | new-hook.ts:129 | Initialize session |
| Context hook fetch | 5000ms | context-hook.ts:32 | Fetch context via curl |
| User message hook | 5000ms | user-message-hook.ts:52 | Fetch context display |
**Problems:**
1. **Health Check Too Aggressive:** 500ms may be too short for loaded systems or slow network
2. **No Platform Adjustment:** Comment says "Windows needs longer timeouts" but values are same
3. **Hook Timeout Variation:** Some hooks use 2s, others use 5s with no clear reasoning
**Recommendations:**
- Increase health check timeout to 1000ms minimum
- Actually implement longer timeouts for Windows
- Standardize hook timeouts to 5000ms across the board
- Make timeouts configurable via settings
---
## Edge Case Analysis
### Handled Well ✅
1. **JSONL Parsing:** summary-hook.ts continues on malformed lines (60-64, 117-121)
2. **Git Not Available:** paths.ts falls back to cwd basename (89-97)
3. **Settings File Missing:** early-settings.ts falls back to env vars and defaults (20-28)
4. **Privacy Tags:** new-hook.ts handles fully-private prompts (99-109)
5. **Tool Skipping:** save-hook.ts filters low-value tools (24-30)
### Missing Edge Case Handling ⚠️
1. **curl Failure:** context-hook.ts has no error handling for curl failures
2. **PM2 Not Installed:** worker-utils.ts assumes pm2 exists globally
3. **PowerShell Restrictions:** worker-utils.ts doesn't check execution policy
4. **Concurrent Worker Starts:** No locking to prevent multiple hooks from starting worker simultaneously
5. **Port Already In Use:** No detection or recovery if worker port is taken
6. **Zombie Processes:** Windows approach doesn't track PIDs, can't detect/kill zombies
---
## Recommendations Summary
### High Priority 🔴
1. **Replace curl with fetch** in context-hook.ts
- Eliminates external dependency
- Consistent with rest of codebase
- Better error handling
2. **Add logging to silent failures**
- early-settings.ts: Log when settings file fails to load
- worker-utils.ts: Log startup failures with details
- worker-utils.ts: Log health check failures at debug level
3. **Fix first-run detection**
- Use marker file instead of node_modules check
- More reliable and intentional
### Medium Priority 🟡
4. **Verify PM2 availability** before attempting to use it
- Check existence before spawn
- Provide clear error message if missing
5. **Implement platform-specific timeouts**
- Actually use longer timeouts on Windows as comment suggests
- Make timeouts configurable
6. **Standardize health endpoints**
- Remove duplicate `/health` endpoint
- Use `/api/health` everywhere
7. **Add path escaping** for Windows PowerShell commands
- Prevent injection issues
- Handle paths with special characters
### Low Priority 🟢
8. **Standardize HTTP timeouts** across all hooks
9. **Add concurrent startup protection** (locking mechanism)
10. **Improve error messages** with actionable recovery steps
---
## Testing Recommendations
### Cross-Platform Testing Needed
1. **Windows Environments:**
- Windows 10 (various versions)
- Windows 11
- Windows Server
- WSL/WSL2
- PowerShell execution policies (Restricted, RemoteSigned, Unrestricted)
2. **Unix Environments:**
- macOS (Intel + Apple Silicon)
- Linux (Ubuntu, Fedora, Arch)
- FreeBSD
3. **Edge Environments:**
- Docker containers
- CI/CD environments
- Systems without git installed
- Systems without curl (or with restricted curl)
- Corporate networks with proxies
- Low-spec systems (slow startup)
### Test Scenarios
1. **Cold Start:** First run with no existing data
2. **Corrupt Settings:** Invalid JSON in settings.json
3. **Missing Dependencies:** No PM2, no git, no curl
4. **Port Conflicts:** Worker port already in use
5. **Rapid Hook Invocations:** Multiple hooks trying to start worker simultaneously
6. **Permission Issues:** Read-only filesystem, restricted execution
7. **Network Issues:** Localhost blocked, slow network
---
## Code Quality Assessment
### Strengths ✅
- Clean separation of concerns (hooks → worker → database)
- No nested try-catch anti-patterns
- Consistent use of modern async/await
- Good use of TypeScript for type safety
- Idempotent database operations
- Clear documentation in critical sections
### Weaknesses ⚠️
- Silent failures hinder debugging
- Inconsistent error handling patterns
- Platform-specific code not fully tested/documented
- Timeout configuration hardcoded and inconsistent
- Some artificial blockers prevent legitimate use cases
### Technical Debt
- Duplicate health endpoints
- curl dependency when fetch available
- PM2 dependency on Unix but not Windows (inconsistent monitoring)
- First-run detection using node_modules existence
- Hardcoded timeout values
---
## Conclusion
The claude-mem smart install and plugin hook system is architecturally sound with a well-designed separation of concerns. However, several cross-platform compatibility issues and silent failure patterns could cause problems in production, particularly on Windows systems or in edge case scenarios.
The highest priority improvements are:
1. Removing the curl dependency
2. Adding proper logging to silent failures
3. Fixing the fragile first-run detection
4. Verifying external dependencies before use
These changes would significantly improve debuggability and cross-platform reliability without requiring major architectural changes.
---
**Analysis Methodology:**
- Systematic review of all TypeScript source files
- Static analysis of error handling patterns
- Cross-platform compatibility assessment
- Edge case identification through code path analysis
- Comparison against best practices and KISS principles
**Files Analyzed:**
- src/hooks/*.ts (6 files)
- src/services/worker-service.ts
- src/services/worker/*.ts (10+ files)
- src/servers/mcp-server.ts
- src/shared/*.ts (worker-utils, early-settings, paths)
- src/utils/*.ts (logger, silent-debug, tag-stripping)
+52 -33
View File
@@ -5,18 +5,26 @@ description: "Environment variables and settings for Claude-Mem"
# Configuration
## Environment Variables
## Settings File
| Variable | Default | Description |
Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created with defaults on first run.
### Core Settings
| Setting | Default | Description |
|-------------------------------|---------------------------------|---------------------------------------|
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory (production default) |
| `CLAUDE_CODE_PATH` | Auto-detected | Path to Claude Code CLI (for Windows) |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_MODEL` | `claude-haiku-4-5` | AI model for processing observations |
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
| `NODE_ENV` | `production` | Environment mode |
| `FORCE_COLOR` | `1` | Enable colored logs |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
### System Configuration
| Setting | Default | Description |
|-------------------------------|---------------------------------|---------------------------------------|
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem` | Data directory location |
| `CLAUDE_MEM_LOG_LEVEL` | `INFO` | Log verbosity (DEBUG, INFO, WARN, ERROR, SILENT) |
| `CLAUDE_MEM_PYTHON_VERSION` | `3.13` | Python version for chroma-mcp |
| `CLAUDE_CODE_PATH` | _(auto-detect)_ | Path to Claude Code CLI (for Windows) |
## Model Configuration
@@ -35,11 +43,11 @@ Configure which AI model processes your observations.
./claude-mem-settings.sh
```
This script manages `CLAUDE_MEM_MODEL` in `~/.claude/settings.json`.
This script manages settings in `~/.claude-mem/settings.json`.
### Manual Configuration
Edit `~/.claude/settings.json`:
Edit `~/.claude-mem/settings.json`:
```json
{
@@ -284,53 +292,64 @@ Token economics help you understand the value of cached observations vs. re-read
### Manual Configuration
Settings are stored in `~/.claude-mem/settings.json`. You can also configure via environment variables in `~/.claude/settings.json`:
Settings are stored in `~/.claude-mem/settings.json`:
```json
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100",
"CLAUDE_MEM_CONTEXT_SESSION_COUNT": "20",
"CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES": "bugfix,decision,discovery",
"CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS": "how-it-works,gotcha",
"CLAUDE_MEM_CONTEXT_FULL_COUNT": "10",
"CLAUDE_MEM_CONTEXT_FULL_FIELD": "narrative",
"CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT": "true",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY": "false",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE": "false"
}
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100",
"CLAUDE_MEM_CONTEXT_SESSION_COUNT": "20",
"CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES": "bugfix,decision,discovery",
"CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS": "how-it-works,gotcha",
"CLAUDE_MEM_CONTEXT_FULL_COUNT": "10",
"CLAUDE_MEM_CONTEXT_FULL_FIELD": "narrative",
"CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS": "true",
"CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT": "true",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY": "false",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE": "false"
}
```
**Note**: The Context Settings Modal is the recommended way to configure these settings, as it provides live preview of changes.
**Note**: The Context Settings Modal (at http://localhost:37777) is the recommended way to configure these settings, as it provides live preview of changes.
## Customization
Settings can be customized in `~/.claude-mem/settings.json`.
### Custom Data Directory
For development or testing, override the data directory:
```bash
export CLAUDE_MEM_DATA_DIR=/custom/path
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_DATA_DIR": "/custom/path"
}
```
### Custom Worker Port
If port 37777 is in use:
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_WORKER_PORT": "38000"
}
```
Then restart the worker:
```bash
export CLAUDE_MEM_WORKER_PORT=38000
npm run worker:restart
```
### Custom Model
Use a different AI model:
Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_MODEL": "claude-opus-4"
}
```
Then restart the worker:
```bash
export CLAUDE_MEM_MODEL=claude-opus-4
npm run worker:restart
```
+12 -6
View File
@@ -1,11 +1,17 @@
#!/usr/bin/env node
import{stdin as p}from"process";import a from"path";import{existsSync as O}from"fs";import{homedir as A}from"os";import{spawnSync as g}from"child_process";import{readFileSync as h,existsSync as y}from"fs";var L=["bugfix","feature","refactor","discovery","decision","change"],D=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var d=L.join(","),f=D.join(",");var T=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:d,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:f,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(!y(t))return this.getAllDefaults();let o=h(t,"utf-8"),r=JSON.parse(o).env||{},i={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))r[E]!==void 0&&(i[E]=r[E]);return i}};var n=a.join(A(),".claude","plugins","marketplaces","thedotmack"),U=500,R=1e3,w=15;function l(){let e=a.join(A(),".claude-mem","settings.json"),t=T.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function C(){try{let e=l();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(U)})).ok}catch{return!1}}async function I(){try{let e=a.join(n,"plugin","scripts","worker-service.cjs");if(!O(e))throw new Error(`Worker script not found at ${e}`);if(process.platform==="win32"){let t=g("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${e}' -WorkingDirectory '${n}' -WindowStyle Hidden`],{cwd:n,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=a.join(n,"ecosystem.config.cjs");if(!O(t))throw new Error(`Ecosystem config not found at ${t}`);let o=a.join(n,"node_modules",".bin","pm2"),s=O(o)?o:"pm2",r=g(s,["start",t],{cwd:n,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;t<w;t++)if(await new Promise(o=>setTimeout(o,R)),await C())return!0;return!1}catch{return!1}}async function m(){if(await C())return;if(!await I()){let t=l();throw new Error(`Worker service failed to start on port ${t}.
import{stdin as A}from"process";import g from"path";import{existsSync as d}from"fs";import{homedir as N}from"os";import{spawnSync as C}from"child_process";import{readFileSync as b,writeFileSync as W,existsSync as x}from"fs";import{join as H}from"path";import{homedir as F}from"os";var $=["bugfix","feature","refactor","discovery","decision","change"],v=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var y=$.join(","),R=v.join(",");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||{}),m=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=p.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;try{let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command){let n=e.command.length>50?e.command.substring(0,50)+"...":e.command;return`${t}(${n})`}if(t==="Read"&&e.file_path){let n=e.file_path.split("/").pop()||e.file_path;return`${t}(${n})`}if(t==="Edit"&&e.file_path){let n=e.file_path.split("/").pop()||e.file_path;return`${t}(${n})`}if(t==="Write"&&e.file_path){let n=e.file_path.split("/").pop()||e.file_path;return`${t}(${n})`}return t}catch{return t}}log(t,r,e,n,s){if(t<this.getLevel())return;let a=new Date().toISOString().replace("T"," ").substring(0,23),f=O[t].padEnd(5),S=r.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let E="";s!=null&&(this.getLevel()===0&&typeof s=="object"?E=`
`+JSON.stringify(s,null,2):E=" "+this.formatData(s));let L="";if(n){let{sessionId:q,sdkSessionId:z,correlationId:Q,...M}=n;Object.keys(M).length>0&&(L=` {${Object.entries(M).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let h=`[${a}] [${f}] [${S}] ${c}${e}${L}${E}`;t===3?console.error(h):console.log(h)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}},_=new m;var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:H(F(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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:R,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 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(!x(t))return this.getAllDefaults();let r=b(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{W(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(s[a]=n[a]);return s}};var l={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function D(o){return process.platform==="win32"?Math.round(o*l.WINDOWS_MULTIPLIER):o}var i=g.join(N(),".claude","plugins","marketplaces","thedotmack"),j=D(l.HEALTH_CHECK),K=l.WORKER_STARTUP_WAIT,X=l.WORKER_STARTUP_RETRIES;function T(){let o=g.join(N(),".claude-mem","settings.json"),t=p.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function I(){try{let o=T();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(j)})).ok}catch(o){return _.debug("SYSTEM","Worker health check failed",{error:o instanceof Error?o.message:String(o),errorType:o?.constructor?.name}),!1}}async function V(){try{let o=g.join(i,"plugin","scripts","worker-service.cjs");if(!d(o))throw new Error(`Worker script not found at ${o}`);if(process.platform==="win32"){let t=o.replace(/'/g,"''"),r=i.replace(/'/g,"''"),e=C("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${t}' -WorkingDirectory '${r}' -WindowStyle Hidden`],{cwd:i,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(e.status!==0)throw new Error(e.stderr||"PowerShell Start-Process failed")}else{let t=g.join(i,"ecosystem.config.cjs");if(!d(t))throw new Error(`Ecosystem config not found at ${t}`);let r=g.join(i,"node_modules",".bin","pm2"),e;if(d(r))e=r;else{if(C("which",["pm2"],{encoding:"utf-8",stdio:"pipe"}).status!==0)throw new Error(`PM2 not found. Install it locally with:
cd ${i}
npm install
Or install globally with: npm install -g pm2`);e="pm2"}let n=C(e,["start",t],{cwd:i,stdio:"pipe",encoding:"utf-8"});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed")}for(let t=0;t<X;t++)if(await new Promise(r=>setTimeout(r,K)),await I())return!0;return!1}catch(o){return _.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:g.join(i,"plugin","scripts","worker-service.cjs"),error:o instanceof Error?o.message:String(o),marketplaceRoot:i}),!1}}async function U(){if(await I())return;if(!await V()){let t=T();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${n}
cd ${i}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as P}from"fs";import{homedir as k}from"os";import{join as W}from"path";var x=W(k(),".claude-mem","silent.log");function c(e,t,o=""){let s=new Date().toISOString(),S=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),N=S?`${S[1].split("/").pop()}:${S[2]}`:"unknown",_=`[${s}] [${N}] ${e}`;if(t!==void 0)try{_+=` ${JSON.stringify(t)}`}catch(u){_+=` [stringify error: ${u}]`}_+=`
`;try{P(x,_)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return o}async function M(e){await m(),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(`
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:o}=e,s=l();try{let r=await fetch(`http://127.0.0.1:${s}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:o}),signal:AbortSignal.timeout(2e3)});if(r.ok){let i=await r.json();c("[cleanup-hook] Session cleanup completed",i)}else c("[cleanup-hook] Session not found or already cleaned up")}catch(r){c("[cleanup-hook] Worker not reachable (non-critical)",{error:r.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(p.isTTY)M(void 0);else{let e="";p.on("data",t=>e+=t),p.on("end",async()=>{let t=e?JSON.parse(e):void 0;await M(t)})}
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as B}from"fs";import{homedir as G}from"os";import{join as Y}from"path";var J=Y(G(),".claude-mem","silent.log");function u(o,t,r=""){let e=new Date().toISOString(),f=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),S=f?`${f[1].split("/").pop()}:${f[2]}`:"unknown",c=`[${e}] [HAPPY-PATH-ERROR] [${S}] ${o}`;if(t!==void 0)try{c+=` ${JSON.stringify(t)}`}catch(E){c+=` [stringify error: ${E}]`}c+=`
`;try{B(J,c)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return r}async function w(o){await U(),u("[cleanup-hook] Hook fired",{session_id:o?.session_id,cwd:o?.cwd,reason:o?.reason}),o||(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:r}=o,e=T();try{let n=await fetch(`http://127.0.0.1:${e}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:r}),signal:AbortSignal.timeout(l.DEFAULT)});if(n.ok){let s=await n.json();u("[cleanup-hook] Session cleanup completed",s)}else u("[cleanup-hook] Session not found or already cleaned up")}catch(n){u("[cleanup-hook] Worker not reachable (non-critical)",{error:n.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(A.isTTY)w(void 0);else{let o="";A.on("data",t=>o+=t),A.on("end",async()=>{let t=o?JSON.parse(o):void 0;await w(t)})}
File diff suppressed because one or more lines are too long
+9 -3
View File
@@ -1,8 +1,14 @@
#!/usr/bin/env node
import R from"path";import{stdin as S}from"process";import{execSync as y}from"child_process";import n from"path";import{existsSync as T}from"fs";import{homedir as f}from"os";import{spawnSync as p}from"child_process";import{readFileSync as N,existsSync as m}from"fs";var M=["bugfix","feature","refactor","discovery","decision","change"],g=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var O=M.join(","),u=g.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:u,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(!m(t))return this.getAllDefaults();let r=N(t,"utf-8"),o=JSON.parse(r).env||{},a={...this.DEFAULTS};for(let c of Object.keys(this.DEFAULTS))o[c]!==void 0&&(a[c]=o[c]);return a}};var s=n.join(f(),".claude","plugins","marketplaces","thedotmack"),d=500,D=1e3,L=15;function E(){let e=n.join(f(),".claude-mem","settings.json"),t=i.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function l(){try{let e=E();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function U(){try{let e=n.join(s,"plugin","scripts","worker-service.cjs");if(!T(e))throw new Error(`Worker script not found at ${e}`);if(process.platform==="win32"){let t=p("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${e}' -WorkingDirectory '${s}' -WindowStyle Hidden`],{cwd:s,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=n.join(s,"ecosystem.config.cjs");if(!T(t))throw new Error(`Ecosystem config not found at ${t}`);let r=n.join(s,"node_modules",".bin","pm2"),_=T(r)?r:"pm2",o=p(_,["start",t],{cwd:s,stdio:"pipe",encoding:"utf-8"});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed")}for(let t=0;t<L;t++)if(await new Promise(r=>setTimeout(r,D)),await l())return!0;return!1}catch{return!1}}async function A(){if(await l())return;if(!await U()){let t=E();throw new Error(`Worker service failed to start on port ${t}.
import V from"path";import{stdin as C}from"process";import l from"path";import{existsSync as O}from"fs";import{homedir as D}from"os";import{spawnSync as m}from"child_process";import{readFileSync as W,writeFileSync as $,existsSync as b}from"fs";import{join as x}from"path";import{homedir as H}from"os";var k=["bugfix","feature","refactor","discovery","decision","change"],v=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var M=k.join(","),h=v.join(",");var g=(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))(g||{}),S=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=_.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=g[t]??1}return this.level}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.getLevel()===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.getLevel())return;let a=new Date().toISOString().replace("T"," ").substring(0,23),f=g[t].padEnd(5),I=e.padEnd(6),T="";n?.correlationId?T=`[${n.correlationId}] `:n?.sessionId&&(T=`[session-${n.sessionId}] `);let u="";s!=null&&(this.getLevel()===0&&typeof s=="object"?u=`
`+JSON.stringify(s,null,2):u=" "+this.formatData(s));let d="";if(n){let{sessionId:G,sdkSessionId:Y,correlationId:J,...L}=n;Object.keys(L).length>0&&(d=` {${Object.entries(L).map(([w,P])=>`${w}=${P}`).join(", ")}}`)}let A=`[${a}] [${f}] [${I}] ${T}${r}${d}${u}`;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`})}},E=new S;var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:x(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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: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 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=W(t,"utf-8"),r=JSON.parse(e),n=r;if(r.env&&typeof r.env=="object"){n=r.env;try{$(t,JSON.stringify(n,null,2),"utf-8"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(s[a]=n[a]);return s}};var c={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function R(o){return process.platform==="win32"?Math.round(o*c.WINDOWS_MULTIPLIER):o}var i=l.join(D(),".claude","plugins","marketplaces","thedotmack"),F=R(c.HEALTH_CHECK),j=c.WORKER_STARTUP_WAIT,K=c.WORKER_STARTUP_RETRIES;function p(){let o=l.join(D(),".claude-mem","settings.json"),t=_.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function y(){try{let o=p();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(F)})).ok}catch(o){return E.debug("SYSTEM","Worker health check failed",{error:o instanceof Error?o.message:String(o),errorType:o?.constructor?.name}),!1}}async function X(){try{let o=l.join(i,"plugin","scripts","worker-service.cjs");if(!O(o))throw new Error(`Worker script not found at ${o}`);if(process.platform==="win32"){let t=o.replace(/'/g,"''"),e=i.replace(/'/g,"''"),r=m("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${t}' -WorkingDirectory '${e}' -WindowStyle Hidden`],{cwd:i,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PowerShell Start-Process failed")}else{let t=l.join(i,"ecosystem.config.cjs");if(!O(t))throw new Error(`Ecosystem config not found at ${t}`);let e=l.join(i,"node_modules",".bin","pm2"),r;if(O(e))r=e;else{if(m("which",["pm2"],{encoding:"utf-8",stdio:"pipe"}).status!==0)throw new Error(`PM2 not found. Install it locally with:
cd ${i}
npm install
Or install globally with: npm install -g pm2`);r="pm2"}let n=m(r,["start",t],{cwd:i,stdio:"pipe",encoding:"utf-8"});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed")}for(let t=0;t<K;t++)if(await new Promise(e=>setTimeout(e,j)),await y())return!0;return!1}catch(o){return E.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:l.join(i,"plugin","scripts","worker-service.cjs"),error:o instanceof Error?o.message:String(o),marketplaceRoot:i}),!1}}async function N(){if(await y())return;if(!await X()){let t=p();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${s}
cd ${i}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}async function C(e){await A();let t=e?.cwd??process.cwd(),r=t?R.basename(t):"unknown-project",o=`http://127.0.0.1:${E()}/api/context/inject?project=${encodeURIComponent(r)}`;return y(`curl -s "${o}"`,{encoding:"utf-8",timeout:5e3}).trim()}var h=process.argv.includes("--colors");if(S.isTTY||h)C(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";S.on("data",t=>e+=t),S.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,r=await C(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
If already running, try: npx pm2 restart claude-mem-worker`)}}async function U(o){await N();let t=o?.cwd??process.cwd(),e=t?V.basename(t):"unknown-project",n=`http://127.0.0.1:${p()}/api/context/inject?project=${encodeURIComponent(e)}`;try{let s=await fetch(n,{signal:AbortSignal.timeout(c.DEFAULT)});if(!s.ok){let f=await s.text();throw new Error(`Failed to fetch context: ${s.status} ${f}`)}return(await s.text()).trim()}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}}var B=process.argv.includes("--colors");if(C.isTTY||B)U(void 0).then(o=>{console.log(o),process.exit(0)});else{let o="";C.on("data",t=>o+=t),C.on("end",async()=>{let t=o.trim()?JSON.parse(o):void 0,e=await U(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e}})),process.exit(0)})}
File diff suppressed because one or more lines are too long
+10 -422
View File
@@ -1,428 +1,16 @@
#!/usr/bin/env node
import _e from"path";import{stdin as j}from"process";import J from"better-sqlite3";import{join as m,dirname as Y,basename as le}from"path";import{homedir as y}from"os";import{existsSync as be,mkdirSync as V}from"fs";import{fileURLToPath as K}from"url";function q(){return typeof __dirname<"u"?__dirname:Y(K(import.meta.url))}var Oe=q(),l=process.env.CLAUDE_MEM_DATA_DIR||m(y(),".claude-mem"),I=process.env.CLAUDE_CONFIG_DIR||m(y(),".claude"),he=m(l,"archives"),fe=m(l,"logs"),Ne=m(l,"trash"),Ie=m(l,"backups"),Ae=m(l,"settings.json"),k=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 x(a){V(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=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:R,correlationId:u,...c}=r;Object.keys(c).length>0&&(_=` {${Object.entries(c).map(([$,G])=>`${$}=${G}`).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 h=class{db;constructor(){x(l),this.db=new J(k),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 (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
applied_at TEXT NOT NULL
)
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
CREATE TABLE IF NOT EXISTS sdk_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT UNIQUE,
project TEXT NOT NULL,
user_prompt TEXT,
started_at TEXT NOT NULL,
started_at_epoch INTEGER NOT NULL,
completed_at TEXT,
completed_at_epoch INTEGER,
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
);
import Q from"path";import{stdin as k}from"process";function $(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 m(n,t,e={}){let r=$(n,t,e);return JSON.stringify(r)}import f from"path";import{existsSync as d}from"fs";import{homedir as U}from"os";import{spawnSync as C}from"child_process";import{readFileSync as H,writeFileSync as W,existsSync as F}from"fs";import{join as j}from"path";import{homedir as K}from"os";var v=["bugfix","feature","refactor","discovery","decision","change"],x=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var M=v.join(","),D=x.join(",");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=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=_.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=T[t]??1}return this.level}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.getLevel()===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 o=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${t}(${o})`}if(t==="Read"&&r.file_path){let o=r.file_path.split("/").pop()||r.file_path;return`${t}(${o})`}if(t==="Edit"&&r.file_path){let o=r.file_path.split("/").pop()||r.file_path;return`${t}(${o})`}if(t==="Write"&&r.file_path){let o=r.file_path.split("/").pop()||r.file_path;return`${t}(${o})`}return t}catch{return t}}log(t,e,r,o,s){if(t<this.getLevel())return;let p=new Date().toISOString().replace("T"," ").substring(0,23),u=T[t].padEnd(5),i=e.padEnd(6),a="";o?.correlationId?a=`[${o.correlationId}] `:o?.sessionId&&(a=`[session-${o.sessionId}] `);let l="";s!=null&&(this.getLevel()===0&&typeof s=="object"?l=`
`+JSON.stringify(s,null,2):l=" "+this.formatData(s));let L="";if(o){let{sessionId:tt,sdkSessionId:et,correlationId:rt,...y}=o;Object.keys(y).length>0&&(L=` {${Object.entries(y).map(([P,b])=>`${P}=${b}`).join(", ")}}`)}let R=`[${p}] [${u}] [${i}] ${a}${r}${L}${l}`;t===3?console.error(R):console.log(R)}debug(t,e,r,o){this.log(0,t,e,r,o)}info(t,e,r,o){this.log(1,t,e,r,o)}warn(t,e,r,o){this.log(2,t,e,r,o)}error(t,e,r,o){this.log(3,t,e,r,o)}dataIn(t,e,r,o){this.info(t,`\u2192 ${e}`,r,o)}dataOut(t,e,r,o){this.info(t,`\u2190 ${e}`,r,o)}success(t,e,r,o){this.info(t,`\u2713 ${e}`,r,o)}failure(t,e,r,o){this.error(t,`\u2717 ${e}`,r,o)}timing(t,e,r,o){this.info(t,`\u23F1 ${e}`,o,{duration:`${r}ms`})}},E=new O;var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:j(K(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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 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(!F(t))return this.getAllDefaults();let e=H(t,"utf-8"),r=JSON.parse(e),o=r;if(r.env&&typeof r.env=="object"){o=r.env;try{W(t,JSON.stringify(o,null,2),"utf-8"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(p){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},p)}}let s={...this.DEFAULTS};for(let p of Object.keys(this.DEFAULTS))o[p]!==void 0&&(s[p]=o[p]);return s}};var g={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function N(n){return process.platform==="win32"?Math.round(n*g.WINDOWS_MULTIPLIER):n}var c=f.join(U(),".claude","plugins","marketplaces","thedotmack"),X=N(g.HEALTH_CHECK),V=g.WORKER_STARTUP_WAIT,B=g.WORKER_STARTUP_RETRIES;function S(){let n=f.join(U(),".claude-mem","settings.json"),t=_.loadFromFile(n);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function I(){try{let n=S();return(await fetch(`http://127.0.0.1:${n}/health`,{signal:AbortSignal.timeout(X)})).ok}catch(n){return E.debug("SYSTEM","Worker health check failed",{error:n instanceof Error?n.message:String(n),errorType:n?.constructor?.name}),!1}}async function G(){try{let n=f.join(c,"plugin","scripts","worker-service.cjs");if(!d(n))throw new Error(`Worker script not found at ${n}`);if(process.platform==="win32"){let t=n.replace(/'/g,"''"),e=c.replace(/'/g,"''"),r=C("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${t}' -WorkingDirectory '${e}' -WindowStyle Hidden`],{cwd:c,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PowerShell Start-Process failed")}else{let t=f.join(c,"ecosystem.config.cjs");if(!d(t))throw new Error(`Ecosystem config not found at ${t}`);let e=f.join(c,"node_modules",".bin","pm2"),r;if(d(e))r=e;else{if(C("which",["pm2"],{encoding:"utf-8",stdio:"pipe"}).status!==0)throw new Error(`PM2 not found. Install it locally with:
cd ${c}
npm install
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
CREATE TABLE IF NOT EXISTS session_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT UNIQUE NOT NULL,
project TEXT NOT NULL,
request TEXT,
investigated TEXT,
learned TEXT,
completed TEXT,
next_steps TEXT,
files_read TEXT,
files_edited TEXT,
notes TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
request TEXT,
investigated TEXT,
learned TEXT,
completed TEXT,
next_steps TEXT,
files_read TEXT,
files_edited TEXT,
notes TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO session_summaries_new
SELECT id, sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, created_at, created_at_epoch
FROM session_summaries
`),this.db.exec("DROP TABLE session_summaries"),this.db.exec("ALTER TABLE session_summaries_new RENAME TO session_summaries"),this.db.exec(`
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
ALTER TABLE observations ADD COLUMN title TEXT;
ALTER TABLE observations ADD COLUMN subtitle TEXT;
ALTER TABLE observations ADD COLUMN facts TEXT;
ALTER TABLE observations ADD COLUMN narrative TEXT;
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE user_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT NOT NULL,
prompt_number INTEGER NOT NULL,
prompt_text TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
);
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
CREATE INDEX idx_user_prompts_lookup ON user_prompts(claude_session_id, prompt_number);
`),this.db.exec(`
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
prompt_text,
content='user_prompts',
content_rowid='id'
);
`),this.db.exec(`
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
VALUES('delete', old.id, old.prompt_text);
END;
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
VALUES('delete', old.id, old.prompt_text);
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(n=>n.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(n=>n.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
FROM session_summaries
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
SELECT
sdk_session_id, request, learned, completed, next_steps,
prompt_number, created_at
FROM session_summaries
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
SELECT type, text, prompt_number, created_at
FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,s)}getAllRecentObservations(e=100){return this.db.prepare(`
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
FROM observations
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e)}getAllRecentSummaries(e=50){return this.db.prepare(`
SELECT id, request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, project, prompt_number,
created_at, created_at_epoch
FROM session_summaries
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e)}getAllRecentUserPrompts(e=100){return this.db.prepare(`
SELECT
up.id,
up.claude_session_id,
s.project,
up.prompt_number,
up.prompt_text,
up.created_at,
up.created_at_epoch
FROM user_prompts up
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
ORDER BY up.created_at_epoch DESC
LIMIT ?
`).all(e)}getAllProjects(){return this.db.prepare(`
SELECT DISTINCT project
FROM sdk_sessions
WHERE project IS NOT NULL AND project != ''
ORDER BY project ASC
`).all().map(t=>t.project)}getLatestUserPrompt(e){return this.db.prepare(`
SELECT
up.*,
s.sdk_session_id,
s.project
FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.claude_session_id = ?
ORDER BY up.created_at_epoch DESC
LIMIT 1
`).get(e)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
SELECT * FROM (
SELECT
s.sdk_session_id,
s.status,
s.started_at,
s.started_at_epoch,
s.user_prompt,
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
FROM sdk_sessions s
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
GROUP BY s.sdk_session_id
ORDER BY s.started_at_epoch DESC
LIMIT ?
)
ORDER BY started_at_epoch ASC
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
SELECT title, subtitle, type, prompt_number
FROM observations
WHERE sdk_session_id = ?
ORDER BY created_at_epoch ASC
`).all(e)}getObservationById(e){return this.db.prepare(`
SELECT *
FROM observations
WHERE id = ?
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT *
FROM observations
WHERE id IN (${i})
ORDER BY created_at_epoch ${o}
${n}
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
FROM session_summaries
WHERE sdk_session_id = ?
ORDER BY created_at_epoch DESC
LIMIT 1
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
SELECT files_read, files_modified
FROM observations
WHERE sdk_session_id = ?
`).all(e),r=new Set,o=new Set;for(let n of t){if(n.files_read)try{let i=JSON.parse(n.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(n.files_modified)try{let i=JSON.parse(n.files_modified);Array.isArray(i)&&i.forEach(p=>o.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)||null}findActiveSDKSession(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, worker_port
FROM sdk_sessions
WHERE claude_session_id = ? AND status = 'active'
LIMIT 1
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
SELECT id
FROM sdk_sessions
WHERE claude_session_id = ?
LIMIT 1
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
UPDATE sdk_sessions
SET status = 'active', user_prompt = ?, worker_port = NULL
WHERE id = ?
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
UPDATE sdk_sessions
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
WHERE id = ?
`).run(e),this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),i=this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, ?, 'active')
`).run(e,e,s,t,r.toISOString(),o);return i.lastInsertRowid===0||i.changes===0?(s&&s.trim()!==""&&this.db.prepare(`
UPDATE sdk_sessions
SET project = ?, user_prompt = ?
WHERE claude_session_id = ?
`).run(s,t,e),this.db.prepare(`
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
`).get(e).id):i.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
`).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
SET worker_port = ?
WHERE id = ?
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
SELECT worker_port
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
INSERT INTO user_prompts
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?)
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}getUserPrompt(e,s){return this.db.prepare(`
SELECT prompt_text
FROM user_prompts
WHERE claude_session_id = ? AND prompt_number = ?
LIMIT 1
`).get(e,s)?.prompt_text??null}storeObservation(e,s,t,r,o=0){let n=new Date,i=n.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let _=this.db.prepare(`
INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o,n.toISOString(),i);return{id:Number(_.lastInsertRowid),createdAtEpoch:i}}storeSummary(e,s,t,r,o=0){let n=new Date,i=n.getTime();this.db.prepare(`
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
`).get(e)||(this.db.prepare(`
INSERT INTO sdk_sessions
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let _=this.db.prepare(`
INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o,n.toISOString(),i);return{id:Number(_.lastInsertRowid),createdAtEpoch:i}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT * FROM session_summaries
WHERE id IN (${i})
ORDER BY created_at_epoch ${o}
${n}
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",n=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
SELECT
up.*,
s.project,
s.sdk_session_id
FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.id IN (${i})
ORDER BY up.created_at_epoch ${o}
${n}
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let n=o?"AND project = ?":"",i=o?[o]:[],p,d;if(e!==null){let S=`
SELECT id, created_at_epoch
FROM observations
WHERE id <= ? ${n}
ORDER BY id DESC
LIMIT ?
`,R=`
SELECT id, created_at_epoch
FROM observations
WHERE id >= ? ${n}
ORDER BY id ASC
LIMIT ?
`;try{let u=this.db.prepare(S).all(e,...i,t+1),c=this.db.prepare(R).all(e,...i,r+1);if(u.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,d=c.length>0?c[c.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let S=`
SELECT created_at_epoch
FROM observations
WHERE created_at_epoch <= ? ${n}
ORDER BY created_at_epoch DESC
LIMIT ?
`,R=`
SELECT created_at_epoch
FROM observations
WHERE created_at_epoch >= ? ${n}
ORDER BY created_at_epoch ASC
LIMIT ?
`;try{let u=this.db.prepare(S).all(s,...i,t),c=this.db.prepare(R).all(s,...i,r+1);if(u.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,d=c.length>0?c[c.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let E=`
SELECT *
FROM observations
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
ORDER BY created_at_epoch ASC
`,_=`
SELECT *
FROM session_summaries
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n}
ORDER BY created_at_epoch ASC
`,T=`
SELECT up.*, s.project, s.sdk_session_id
FROM user_prompts up
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")}
ORDER BY up.created_at_epoch ASC
`;try{let S=this.db.prepare(E).all(p,d,...i),R=this.db.prepare(_).all(p,d,...i),u=this.db.prepare(T).all(p,d,...i);return{observations:S,sessions:R.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 C(a,e,s={}){let t=Q(a,e,s);return JSON.stringify(t)}import O from"path";import{existsSync as D}from"fs";import{homedir as P}from"os";import{spawnSync as F}from"child_process";import{readFileSync as ee,existsSync as se}from"fs";var z=["bugfix","feature","refactor","discovery","decision","change"],Z=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var U=z.join(","),w=Z.join(",");var f=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:w,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(!se(e))return this.getAllDefaults();let s=ee(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 g=O.join(P(),".claude","plugins","marketplaces","thedotmack"),te=500,re=1e3,oe=15;function N(){let a=O.join(P(),".claude-mem","settings.json"),e=f.loadFromFile(a);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}async function X(){try{let a=N();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(te)})).ok}catch{return!1}}async function ne(){try{let a=O.join(g,"plugin","scripts","worker-service.cjs");if(!D(a))throw new Error(`Worker script not found at ${a}`);if(process.platform==="win32"){let e=F("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${a}' -WorkingDirectory '${g}' -WindowStyle Hidden`],{cwd:g,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(e.status!==0)throw new Error(e.stderr||"PowerShell Start-Process failed")}else{let e=O.join(g,"ecosystem.config.cjs");if(!D(e))throw new Error(`Ecosystem config not found at ${e}`);let s=O.join(g,"node_modules",".bin","pm2"),t=D(s)?s:"pm2",r=F(t,["start",e],{cwd:g,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let e=0;e<oe;e++)if(await new Promise(s=>setTimeout(s,re)),await X())return!0;return!1}catch{return!1}}async function H(){if(await X())return;if(!await ne()){let e=N();throw new Error(`Worker service failed to start on port ${e}.
Or install globally with: npm install -g pm2`);r="pm2"}let o=C(r,["start",t],{cwd:c,stdio:"pipe",encoding:"utf-8"});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed")}for(let t=0;t<B;t++)if(await new Promise(e=>setTimeout(e,V)),await I())return!0;return!1}catch(n){return E.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:f.join(c,"plugin","scripts","worker-service.cjs"),error:n instanceof Error?n.message:String(n),marketplaceRoot:c}),!1}}async function w(){if(await I())return;if(!await G()){let t=S();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${g}
cd ${c}
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 b(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+=`
`;try{ie(ce,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var B=100;function de(a){let e=(a.match(/<private>/g)||[]).length,s=(a.match(/<claude-mem-context>/g)||[]).length;return e+s}function W(a){if(typeof a!="string")return b("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=de(a);return e>B&&b("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:B,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 H(),!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;b("[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);b("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s});let o=new h,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=W(t);if(!p||p.trim()===""){b("[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=N(),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 v="";j.on("data",a=>v+=a);j.on("end",async()=>{let a=v?JSON.parse(v):void 0;await ue(a)});
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as Y}from"fs";import{homedir as J}from"os";import{join as q}from"path";var z=q(J(),".claude-mem","silent.log");function h(n,t,e=""){let r=new Date().toISOString(),u=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),i=u?`${u[1].split("/").pop()}:${u[2]}`:"unknown",a=`[${r}] [HAPPY-PATH-ERROR] [${i}] ${n}`;if(t!==void 0)try{a+=` ${JSON.stringify(t)}`}catch(l){a+=` [stringify error: ${l}]`}a+=`
`;try{Y(z,a)}catch(l){console.error("[silent-debug] Failed to write to log:",l)}return e}async function Z(n){if(await w(),!n)throw new Error("newHook requires input");let{session_id:t,cwd:e,prompt:r}=n;h("[new-hook] Input received",{session_id:t,cwd:e,cwd_type:typeof e,cwd_length:e?.length,has_cwd:!!e,prompt_length:r?.length});let o=Q.basename(e);h("[new-hook] Project extracted",{project:o,project_type:typeof o,project_length:o?.length,is_empty:o==="",cwd_was:e});let s=S(),p,u;try{let i=await fetch(`http://127.0.0.1:${s}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,project:o,prompt:r}),signal:AbortSignal.timeout(5e3)});if(!i.ok){let l=await i.text();throw new Error(`Failed to initialize session: ${i.status} ${l}`)}let a=await i.json();if(p=a.sessionDbId,u=a.promptNumber,a.skipped&&a.reason==="private"){console.error(`[new-hook] Session ${p}, prompt #${u} (fully private - skipped)`),console.log(m("UserPromptSubmit",!0));return}console.error(`[new-hook] Session ${p}, prompt #${u}`)}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(m("UserPromptSubmit",!0))}var A="";k.on("data",n=>A+=n);k.on("end",async()=>{let n=A?JSON.parse(A):void 0;await Z(n)});
+11 -5
View File
@@ -1,10 +1,16 @@
#!/usr/bin/env node
import{stdin as D}from"process";function w(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=w(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||{}),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),f=T[t].padEnd(5),i=e.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let O="";s!=null&&(this.level===0&&typeof s=="object"?O=`
`+JSON.stringify(s,null,2):O=" "+this.formatData(s));let d="";if(r){let{sessionId:j,sdkSessionId:K,correlationId:V,...h}=r;Object.keys(h).length>0&&(d=` {${Object.entries(h).map(([I,P])=>`${I}=${P}`).join(", ")}}`)}let A=`[${a}] [${f}] [${i}] ${c}${o}${d}${O}`;t===3?console.error(A):console.log(A)}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`})}},p=new g;import E from"path";import{existsSync as m}from"fs";import{homedir as N}from"os";import{spawnSync as R}from"child_process";import{readFileSync as v,existsSync as $}from"fs";var k=["bugfix","feature","refactor","discovery","decision","change"],b=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var y=k.join(","),M=b.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: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(!$(t))return this.getAllDefaults();let e=v(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 u=E.join(N(),".claude","plugins","marketplaces","thedotmack"),x=500,H=1e3,W=15;function l(){let n=E.join(N(),".claude-mem","settings.json"),t=_.loadFromFile(n);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function L(){try{let n=l();return(await fetch(`http://127.0.0.1:${n}/health`,{signal:AbortSignal.timeout(x)})).ok}catch{return!1}}async function F(){try{let n=E.join(u,"plugin","scripts","worker-service.cjs");if(!m(n))throw new Error(`Worker script not found at ${n}`);if(process.platform==="win32"){let t=R("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${n}' -WorkingDirectory '${u}' -WindowStyle Hidden`],{cwd:u,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=E.join(u,"ecosystem.config.cjs");if(!m(t))throw new Error(`Ecosystem config not found at ${t}`);let e=E.join(u,"node_modules",".bin","pm2"),o=m(e)?e:"pm2",r=R(o,["start",t],{cwd:u,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;t<W;t++)if(await new Promise(e=>setTimeout(e,H)),await L())return!0;return!1}catch{return!1}}async function U(){if(await L())return;if(!await F()){let t=l();throw new Error(`Worker service failed to start on port ${t}.
import{stdin as k}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 T(n,t,e={}){let r=v(n,t,e);return JSON.stringify(r)}import{readFileSync as x,writeFileSync as W,existsSync as F}from"fs";import{join as K}from"path";import{homedir as j}from"os";var $=["bugfix","feature","refactor","discovery","decision","change"],H=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var R=$.join(","),y=H.join(",");var f=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:K(j(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:y,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 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(!F(t))return this.getAllDefaults();let e=x(t,"utf-8"),r=JSON.parse(e),o=r;if(r.env&&typeof r.env=="object"){o=r.env;try{W(t,JSON.stringify(o,null,2),"utf-8"),l.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){l.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))o[a]!==void 0&&(s[a]=o[a]);return s}};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||{}),m=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=f.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}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.getLevel()===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 o=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${t}(${o})`}if(t==="Read"&&r.file_path){let o=r.file_path.split("/").pop()||r.file_path;return`${t}(${o})`}if(t==="Edit"&&r.file_path){let o=r.file_path.split("/").pop()||r.file_path;return`${t}(${o})`}if(t==="Write"&&r.file_path){let o=r.file_path.split("/").pop()||r.file_path;return`${t}(${o})`}return t}catch{return t}}log(t,e,r,o,s){if(t<this.getLevel())return;let a=new Date().toISOString().replace("T"," ").substring(0,23),u=O[t].padEnd(5),i=e.padEnd(6),c="";o?.correlationId?c=`[${o.correlationId}] `:o?.sessionId&&(c=`[session-${o.sessionId}] `);let E="";s!=null&&(this.getLevel()===0&&typeof s=="object"?E=`
`+JSON.stringify(s,null,2):E=" "+this.formatData(s));let L="";if(o){let{sessionId:tt,sdkSessionId:et,correlationId:rt,...M}=o;Object.keys(M).length>0&&(L=` {${Object.entries(M).map(([P,b])=>`${P}=${b}`).join(", ")}}`)}let h=`[${a}] [${u}] [${i}] ${c}${r}${L}${E}`;t===3?console.error(h):console.log(h)}debug(t,e,r,o){this.log(0,t,e,r,o)}info(t,e,r,o){this.log(1,t,e,r,o)}warn(t,e,r,o){this.log(2,t,e,r,o)}error(t,e,r,o){this.log(3,t,e,r,o)}dataIn(t,e,r,o){this.info(t,`\u2192 ${e}`,r,o)}dataOut(t,e,r,o){this.info(t,`\u2190 ${e}`,r,o)}success(t,e,r,o){this.info(t,`\u2713 ${e}`,r,o)}failure(t,e,r,o){this.error(t,`\u2717 ${e}`,r,o)}timing(t,e,r,o){this.info(t,`\u23F1 ${e}`,o,{duration:`${r}ms`})}},l=new m;import g from"path";import{existsSync as d}from"fs";import{homedir as U}from"os";import{spawnSync as C}from"child_process";var _={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function D(n){return process.platform==="win32"?Math.round(n*_.WINDOWS_MULTIPLIER):n}var p=g.join(U(),".claude","plugins","marketplaces","thedotmack"),X=D(_.HEALTH_CHECK),V=_.WORKER_STARTUP_WAIT,B=_.WORKER_STARTUP_RETRIES;function S(){let n=g.join(U(),".claude-mem","settings.json"),t=f.loadFromFile(n);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function N(){try{let n=S();return(await fetch(`http://127.0.0.1:${n}/health`,{signal:AbortSignal.timeout(X)})).ok}catch(n){return l.debug("SYSTEM","Worker health check failed",{error:n instanceof Error?n.message:String(n),errorType:n?.constructor?.name}),!1}}async function G(){try{let n=g.join(p,"plugin","scripts","worker-service.cjs");if(!d(n))throw new Error(`Worker script not found at ${n}`);if(process.platform==="win32"){let t=n.replace(/'/g,"''"),e=p.replace(/'/g,"''"),r=C("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${t}' -WorkingDirectory '${e}' -WindowStyle Hidden`],{cwd:p,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PowerShell Start-Process failed")}else{let t=g.join(p,"ecosystem.config.cjs");if(!d(t))throw new Error(`Ecosystem config not found at ${t}`);let e=g.join(p,"node_modules",".bin","pm2"),r;if(d(e))r=e;else{if(C("which",["pm2"],{encoding:"utf-8",stdio:"pipe"}).status!==0)throw new Error(`PM2 not found. Install it locally with:
cd ${p}
npm install
Or install globally with: npm install -g pm2`);r="pm2"}let o=C(r,["start",t],{cwd:p,stdio:"pipe",encoding:"utf-8"});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed")}for(let t=0;t<B;t++)if(await new Promise(e=>setTimeout(e,V)),await N())return!0;return!1}catch(n){return l.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:g.join(p,"plugin","scripts","worker-service.cjs"),error:n instanceof Error?n.message:String(n),marketplaceRoot:p}),!1}}async function I(){if(await N())return;if(!await G()){let t=S();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${u}
cd ${p}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}var X=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function B(n){if(await U(),!n)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:r,tool_response:s}=n;if(X.has(o)){console.log(S("PostToolUse",!0));return}let a=l(),f=p.formatTool(o,r);p.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:r,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let c=await i.text();throw p.failure("HOOK","Failed to send observation",{status:i.status},c),new Error(`Failed to send observation to worker: ${i.status} ${c}`)}p.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 C="";D.on("data",n=>C+=n);D.on("end",async()=>{let n=C?JSON.parse(C):void 0;await B(n)});
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as Y}from"fs";import{homedir as J}from"os";import{join as q}from"path";var Q=q(J(),".claude-mem","silent.log");function w(n,t,e=""){let r=new Date().toISOString(),u=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),i=u?`${u[1].split("/").pop()}:${u[2]}`:"unknown",c=`[${r}] [HAPPY-PATH-ERROR] [${i}] ${n}`;if(t!==void 0)try{c+=` ${JSON.stringify(t)}`}catch(E){c+=` [stringify error: ${E}]`}c+=`
`;try{Y(Q,c)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return e}var z=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function Z(n){if(await I(),!n)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:r,tool_input:o,tool_response:s}=n;if(z.has(r)){console.log(T("PostToolUse",!0));return}let a=S(),u=l.formatTool(r,o);l.dataIn("HOOK",`PostToolUse: ${u}`,{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:r,tool_input:o,tool_response:s,cwd:w("Missing cwd in PostToolUse hook input",{session_id:t,tool_name:r},e||"")}),signal:AbortSignal.timeout(_.DEFAULT)});if(!i.ok){let c=await i.text();throw l.failure("HOOK","Failed to send observation",{status:i.status},c),new Error(`Failed to send observation to worker: ${i.status} ${c}`)}l.debug("HOOK","Observation sent successfully",{toolName:r})}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(T("PostToolUse",!0))}var A="";k.on("data",n=>A+=n);k.on("end",async()=>{let n=A?JSON.parse(A):void 0;await Z(n)});
+15 -9
View File
@@ -1,16 +1,22 @@
#!/usr/bin/env node
import{stdin as U}from"process";import{readFileSync as I,existsSync as x}from"fs";function P(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 A(o,t,e={}){let n=P(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 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),c=O[t].padEnd(5),p=e.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let g="";s!=null&&(this.level===0&&typeof s=="object"?g=`
`+JSON.stringify(s,null,2):g=" "+this.formatData(s));let d="";if(r){let{sessionId:G,sdkSessionId:Y,correlationId:J,...C}=r;Object.keys(C).length>0&&(d=` {${Object.entries(C).map(([w,k])=>`${w}=${k}`).join(", ")}}`)}let y=`[${i}] [${c}] [${p}] ${_}${n}${d}${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`})}},u=new S;import E from"path";import{existsSync as m}from"fs";import{homedir as L}from"os";import{spawnSync as N}from"child_process";import{readFileSync as v,existsSync as H}from"fs";var b=["bugfix","feature","refactor","discovery","decision","change"],$=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var h=b.join(","),M=$.join(",");var f=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:h,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(!H(t))return this.getAllDefaults();let e=v(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 a=E.join(L(),".claude","plugins","marketplaces","thedotmack"),W=500,F=1e3,j=15;function l(){let o=E.join(L(),".claude-mem","settings.json"),t=f.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function R(){try{let o=l();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function X(){try{let o=E.join(a,"plugin","scripts","worker-service.cjs");if(!m(o))throw new Error(`Worker script not found at ${o}`);if(process.platform==="win32"){let t=N("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${o}' -WorkingDirectory '${a}' -WindowStyle Hidden`],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=E.join(a,"ecosystem.config.cjs");if(!m(t))throw new Error(`Ecosystem config not found at ${t}`);let e=E.join(a,"node_modules",".bin","pm2"),n=m(e)?e:"pm2",r=N(n,["start",t],{cwd:a,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;t<j;t++)if(await new Promise(e=>setTimeout(e,F)),await R())return!0;return!1}catch{return!1}}async function D(){if(await R())return;if(!await X()){let t=l();throw new Error(`Worker service failed to start on port ${t}.
import{stdin as k}from"process";import{readFileSync as P,existsSync as x}from"fs";function H(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 L(o,t,e={}){let r=H(o,t,e);return JSON.stringify(r)}import{readFileSync as F,writeFileSync as K,existsSync as j}from"fs";import{join as X}from"path";import{homedir as V}from"os";var v=["bugfix","feature","refactor","discovery","decision","change"],W=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var M=v.join(","),R=W.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:X(V(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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:R,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 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(!j(t))return this.getAllDefaults();let e=F(t,"utf-8"),r=JSON.parse(e),n=r;if(r.env&&typeof r.env=="object"){n=r.env;try{K(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(s[i]=n[i]);return s}};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||{}),O=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=_.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=m[t]??1}return this.level}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.getLevel()===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.getLevel())return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=m[t].padEnd(5),l=e.padEnd(6),u="";n?.correlationId?u=`[${n.correlationId}] `:n?.sessionId&&(u=`[session-${n.sessionId}] `);let E="";s!=null&&(this.getLevel()===0&&typeof s=="object"?E=`
`+JSON.stringify(s,null,2):E=" "+this.formatData(s));let C="";if(n){let{sessionId:nt,sdkSessionId:ot,correlationId:st,...h}=n;Object.keys(h).length>0&&(C=` {${Object.entries(h).map(([b,$])=>`${b}=${$}`).join(", ")}}`)}let A=`[${i}] [${a}] [${l}] ${u}${r}${C}${E}`;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`})}},c=new O;import g from"path";import{existsSync as T}from"fs";import{homedir as N}from"os";import{spawnSync as d}from"child_process";var f={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function D(o){return process.platform==="win32"?Math.round(o*f.WINDOWS_MULTIPLIER):o}var p=g.join(N(),".claude","plugins","marketplaces","thedotmack"),B=D(f.HEALTH_CHECK),G=f.WORKER_STARTUP_WAIT,Y=f.WORKER_STARTUP_RETRIES;function S(){let o=g.join(N(),".claude-mem","settings.json"),t=_.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function U(){try{let o=S();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(B)})).ok}catch(o){return c.debug("SYSTEM","Worker health check failed",{error:o instanceof Error?o.message:String(o),errorType:o?.constructor?.name}),!1}}async function J(){try{let o=g.join(p,"plugin","scripts","worker-service.cjs");if(!T(o))throw new Error(`Worker script not found at ${o}`);if(process.platform==="win32"){let t=o.replace(/'/g,"''"),e=p.replace(/'/g,"''"),r=d("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${t}' -WorkingDirectory '${e}' -WindowStyle Hidden`],{cwd:p,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PowerShell Start-Process failed")}else{let t=g.join(p,"ecosystem.config.cjs");if(!T(t))throw new Error(`Ecosystem config not found at ${t}`);let e=g.join(p,"node_modules",".bin","pm2"),r;if(T(e))r=e;else{if(d("which",["pm2"],{encoding:"utf-8",stdio:"pipe"}).status!==0)throw new Error(`PM2 not found. Install it locally with:
cd ${p}
npm install
Or install globally with: npm install -g pm2`);r="pm2"}let n=d(r,["start",t],{cwd:p,stdio:"pipe",encoding:"utf-8"});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed")}for(let t=0;t<Y;t++)if(await new Promise(e=>setTimeout(e,G)),await U())return!0;return!1}catch(o){return c.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:g.join(p,"plugin","scripts","worker-service.cjs"),error:o instanceof Error?o.message:String(o),marketplaceRoot:p}),!1}}async function I(){if(await U())return;if(!await J()){let t=S();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${a}
cd ${p}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}function B(o){if(!o||!x(o))return"";try{let t=I(o,"utf-8").trim();if(!t)return"";let e=t.split(`
`);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(c=>c.type==="text").map(c=>c.text).join(`
`)}}catch{continue}}catch(t){u.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}function K(o){if(!o||!x(o))return"";try{let t=I(o,"utf-8").trim();if(!t)return"";let e=t.split(`
`);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(p=>p.type==="text").map(p=>p.text).join(`
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as q}from"fs";import{homedir as z}from"os";import{join as Q}from"path";var Z=Q(z(),".claude-mem","silent.log");function w(o,t,e=""){let r=new Date().toISOString(),a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u=`[${r}] [HAPPY-PATH-ERROR] [${l}] ${o}`;if(t!==void 0)try{u+=` ${JSON.stringify(t)}`}catch(E){u+=` [stringify error: ${E}]`}u+=`
`;try{q(Z,u)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return e}function tt(o){if(!o||!x(o))return"";try{let t=P(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(a=>a.type==="text").map(a=>a.text).join(`
`)}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}function et(o){if(!o||!x(o))return"";try{let t=P(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(l=>l.type==="text").map(l=>l.text).join(`
`)),s=s.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),s=s.replace(/\n{3,}/g,`
`).trim(),s}}catch{continue}}catch(t){u.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function V(o){if(await D(),!o)throw new Error("summaryHook requires input");let{session_id:t}=o,e=l(),n=B(o.transcript_path||""),r=K(o.transcript_path||"");u.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 u.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}u.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(A("Stop",!0))}var T="";U.on("data",o=>T+=o);U.on("end",async()=>{let o=T?JSON.parse(T):void 0;await V(o)});
`).trim(),s}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function rt(o){if(await I(),!o)throw new Error("summaryHook requires input");let{session_id:t}=o,e=S(),r=w("Missing transcript_path in Stop hook input",{session_id:t},o.transcript_path||""),n=tt(r),s=et(r);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!n,hasLastAssistantMessage:!!s});try{let i=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:s}),signal:AbortSignal.timeout(f.DEFAULT)});if(!i.ok){let a=await i.text();throw c.failure("HOOK","Failed to generate summary",{status:i.status},a),new Error(`Failed to request summary from worker: ${i.status} ${a}`)}c.debug("HOOK","Summary request sent successfully")}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}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(L("Stop",!0))}var y="";k.on("data",o=>y+=o);k.on("end",async()=>{let o=y?JSON.parse(y):void 0;await rt(o)});
+33 -27
View File
@@ -1,29 +1,17 @@
#!/usr/bin/env node
import{join as M,basename as W}from"path";import{homedir as v}from"os";import{existsSync as x}from"fs";import i from"path";import{existsSync as u}from"fs";import{homedir as f}from"os";import{spawnSync as d}from"child_process";import{readFileSync as y,existsSync as w}from"fs";var L=["bugfix","feature","refactor","discovery","decision","change"],U=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var S=L.join(","),m=U.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:S,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 o=this.get(t);return parseInt(o,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!w(t))return this.getAllDefaults();let o=y(t,"utf-8"),r=JSON.parse(o).env||{},c={...this.DEFAULTS};for(let s of Object.keys(this.DEFAULTS))r[s]!==void 0&&(c[s]=r[s]);return c}};var n=i.join(f(),".claude","plugins","marketplaces","thedotmack"),R=500,P=1e3,I=15;function _(){let e=i.join(f(),".claude-mem","settings.json"),t=E.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function C(){try{let e=_();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(R)})).ok}catch{return!1}}async function k(){try{let e=i.join(n,"plugin","scripts","worker-service.cjs");if(!u(e))throw new Error(`Worker script not found at ${e}`);if(process.platform==="win32"){let t=d("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${e}' -WorkingDirectory '${n}' -WindowStyle Hidden`],{cwd:n,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=i.join(n,"ecosystem.config.cjs");if(!u(t))throw new Error(`Ecosystem config not found at ${t}`);let o=i.join(n,"node_modules",".bin","pm2"),a=u(o)?o:"pm2",r=d(a,["start",t],{cwd:n,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;t<I;t++)if(await new Promise(o=>setTimeout(o,P)),await C())return!0;return!1}catch{return!1}}async function A(){if(await C())return;if(!await k()){let t=_();throw new Error(`Worker service failed to start on port ${t}.
import{basename as B}from"path";import l from"path";import{existsSync as h}from"fs";import{homedir as U}from"os";import{spawnSync as A}from"child_process";import{readFileSync as b,writeFileSync as $,existsSync as x}from"fs";import{join as H}from"path";import{homedir as F}from"os";var P=["bugfix","feature","refactor","discovery","decision","change"],W=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var D=P.join(","),y=W.join(",");var C=(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))(C||{}),d=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=E.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=C[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;try{let n=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&n.command){let e=n.command.length>50?n.command.substring(0,50)+"...":n.command;return`${t}(${e})`}if(t==="Read"&&n.file_path){let e=n.file_path.split("/").pop()||n.file_path;return`${t}(${e})`}if(t==="Edit"&&n.file_path){let e=n.file_path.split("/").pop()||n.file_path;return`${t}(${e})`}if(t==="Write"&&n.file_path){let e=n.file_path.split("/").pop()||n.file_path;return`${t}(${e})`}return t}catch{return t}}log(t,r,n,e,s){if(t<this.getLevel())return;let a=new Date().toISOString().replace("T"," ").substring(0,23),g=C[t].padEnd(5),_=r.padEnd(6),S="";e?.correlationId?S=`[${e.correlationId}] `:e?.sessionId&&(S=`[session-${e.sessionId}] `);let u="";s!=null&&(this.getLevel()===0&&typeof s=="object"?u=`
`+JSON.stringify(s,null,2):u=" "+this.formatData(s));let p="";if(e){let{sessionId:M,sdkSessionId:w,correlationId:L,...m}=e;Object.keys(m).length>0&&(p=` {${Object.entries(m).map(([v,k])=>`${v}=${k}`).join(", ")}}`)}let T=`[${a}] [${g}] [${_}] ${S}${n}${p}${u}`;t===3?console.error(T):console.log(T)}debug(t,r,n,e){this.log(0,t,r,n,e)}info(t,r,n,e){this.log(1,t,r,n,e)}warn(t,r,n,e){this.log(2,t,r,n,e)}error(t,r,n,e){this.log(3,t,r,n,e)}dataIn(t,r,n,e){this.info(t,`\u2192 ${r}`,n,e)}dataOut(t,r,n,e){this.info(t,`\u2190 ${r}`,n,e)}success(t,r,n,e){this.info(t,`\u2713 ${r}`,n,e)}failure(t,r,n,e){this.error(t,`\u2717 ${r}`,n,e)}timing(t,r,n,e){this.info(t,`\u23F1 ${r}`,e,{duration:`${n}ms`})}},c=new d;var E=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:H(F(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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:D,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:y,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 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(!x(t))return this.getAllDefaults();let r=b(t,"utf-8"),n=JSON.parse(r),e=n;if(n.env&&typeof n.env=="object"){e=n.env;try{$(t,JSON.stringify(e,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))e[a]!==void 0&&(s[a]=e[a]);return s}};var f={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function R(o){return process.platform==="win32"?Math.round(o*f.WINDOWS_MULTIPLIER):o}var i=l.join(U(),".claude","plugins","marketplaces","thedotmack"),j=R(f.HEALTH_CHECK),K=f.WORKER_STARTUP_WAIT,V=f.WORKER_STARTUP_RETRIES;function O(){let o=l.join(U(),".claude-mem","settings.json"),t=E.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function N(){try{let o=O();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(j)})).ok}catch(o){return c.debug("SYSTEM","Worker health check failed",{error:o instanceof Error?o.message:String(o),errorType:o?.constructor?.name}),!1}}async function X(){try{let o=l.join(i,"plugin","scripts","worker-service.cjs");if(!h(o))throw new Error(`Worker script not found at ${o}`);if(process.platform==="win32"){let t=o.replace(/'/g,"''"),r=i.replace(/'/g,"''"),n=A("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${t}' -WorkingDirectory '${r}' -WindowStyle Hidden`],{cwd:i,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PowerShell Start-Process failed")}else{let t=l.join(i,"ecosystem.config.cjs");if(!h(t))throw new Error(`Ecosystem config not found at ${t}`);let r=l.join(i,"node_modules",".bin","pm2"),n;if(h(r))n=r;else{if(A("which",["pm2"],{encoding:"utf-8",stdio:"pipe"}).status!==0)throw new Error(`PM2 not found. Install it locally with:
cd ${i}
npm install
Or install globally with: npm install -g pm2`);n="pm2"}let e=A(n,["start",t],{cwd:i,stdio:"pipe",encoding:"utf-8"});if(e.status!==0)throw new Error(e.stderr||"PM2 start failed")}for(let t=0;t<V;t++)if(await new Promise(r=>setTimeout(r,K)),await N())return!0;return!1}catch(o){return c.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:l.join(i,"plugin","scripts","worker-service.cjs"),error:o instanceof Error?o.message:String(o),marketplaceRoot:i}),!1}}async function I(){if(await N())return;if(!await X()){let t=O();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${n}
cd ${i}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}var b=M(v(),".claude","plugins","marketplaces","thedotmack"),F=M(b,"node_modules");x(F)||(console.error(`
---
\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.
---
\u26A0\uFE0F Claude-Mem: First-Time Setup
Dependencies have been installed in the background. This only happens once.
\u{1F4A1} TIPS:
\u2022 Memories will start generating while you work
\u2022 Use /init to write or update your CLAUDE.md for better project context
\u2022 Try /clear after one session to see what context looks like
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`),process.exit(3));try{await A();let e=_(),t=W(process.cwd()),o=await fetch(`http://127.0.0.1:${e}/api/context/inject?project=${encodeURIComponent(t)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!o.ok)throw new Error(`Worker error ${o.status}`);let a=await o.text(),r=new Date,c=new Date("2025-12-06T00:00:00Z"),s=new Date("2025-12-05T05:00:00Z"),l="";r<s&&(l=`
If already running, try: npx pm2 restart claude-mem-worker`)}}try{await I();let o=O(),t=B(process.cwd()),r=await fetch(`http://127.0.0.1:${o}/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 n=await r.text(),e=new Date,s=new Date("2025-12-06T00:00:00Z"),a=new Date("2025-12-05T05:00:00Z"),g="";e<a&&(g=`
\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}
@@ -33,19 +21,37 @@ This message was not added to your startup context, so you can continue working
\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}
`);let T="";if(r<c){let g=r.getUTCHours()*60+r.getUTCMinutes(),p=Math.floor((g-300+1440)%1440/60),O=r.getUTCDate(),h=r.getUTCMonth(),N=r.getUTCFullYear()===2025&&h===11&&O>=1&&O<=5,D=p>=17&&p<19;N&&D?T=`
`);let _="";if(e<s){let u=e.getUTCHours()*60+e.getUTCMinutes(),p=Math.floor((u-300+1440)%1440/60),T=e.getUTCDate(),M=e.getUTCMonth(),L=e.getUTCFullYear()===2025&&M===11&&T>=1&&T<=5,m=p>=17&&p<19;L&&m?_=`
\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
`}console.error(`
\u{1F4DD} Claude-Mem Context Loaded
\u2139\uFE0F Note: This appears as stderr but is informational only
`+a+`
`+n+`
\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`+l+T+`
\u{1F4FA} Watch live in browser http://localhost:${e}/
`)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
\u{1F4AC} Community https://discord.gg/J4wttp9vDu`+g+_+`
\u{1F4FA} Watch live in browser http://localhost:${o}/
`)}catch{console.error(`
---
\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.
---
\u26A0\uFE0F Claude-Mem: First-Time Setup
Dependencies are installing in the background. This only happens once.
\u{1F4A1} TIPS:
\u2022 Memories will start generating while you work
\u2022 Use /init to write or update your CLAUDE.md for better project context
\u2022 Try /clear after one session to see what context looks like
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`)}process.exit(3);
File diff suppressed because one or more lines are too long
@@ -87,7 +87,7 @@ Quick fixes for frequently encountered claude-mem problems.
**Fix:**
1. Check the observation count setting:
```bash
grep CLAUDE_MEM_CONTEXT_OBSERVATIONS ~/.claude/settings.json
grep CLAUDE_MEM_CONTEXT_OBSERVATIONS ~/.claude-mem/settings.json
```
2. Default is 50 observations - you can adjust this:
@@ -188,7 +188,7 @@ echo " Health check: $(curl -s http://127.0.0.1:37777/health 2>/dev/null || ec
echo ""
echo "5. Configuration"
echo " Port setting: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_WORKER_PORT || echo 'default (37777)')"
echo " Observation count: $(cat ~/.claude/settings.json 2>/dev/null | grep CLAUDE_MEM_CONTEXT_OBSERVATIONS || echo 'default (50)')"
echo " Observation count: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_CONTEXT_OBSERVATIONS || echo 'default (50)')"
echo ""
echo "6. Recent Activity"
echo " Latest observation: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT created_at FROM observations ORDER BY created_at DESC LIMIT 1;' 2>/dev/null || echo 'N/A')"
@@ -85,7 +85,7 @@ cat ~/.claude/settings.json
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
# Change context observation count
# Edit ~/.claude/settings.json and add:
# Edit ~/.claude-mem/settings.json and add:
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "25"
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Find Silent Failure Patterns
#
# This script searches for defensive OR patterns (|| '' || null || undefined)
# that should potentially use happy_path_error__with_fallback instead.
#
# Usage: ./scripts/find-silent-failures.sh
echo "=================================================="
echo "Searching for defensive OR patterns in src/"
echo "These MAY be silent failures that should log errors"
echo "=================================================="
echo ""
echo "🔍 Searching for: || ''"
echo "---"
grep -rn "|| ''" src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "🔍 Searching for: || \"\""
echo "---"
grep -rn '|| ""' src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "🔍 Searching for: || null"
echo "---"
grep -rn "|| null" src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "🔍 Searching for: || undefined"
echo "---"
grep -rn "|| undefined" src/ --include="*.ts" --color=always || echo " (none found)"
echo ""
echo "=================================================="
echo "Review each match and determine if it should use:"
echo " happy_path_error__with_fallback('description', data, fallback)"
echo "=================================================="
+7 -6
View File
@@ -8,7 +8,8 @@
import { stdin } from 'process';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
export interface SessionEndInput {
session_id: string;
@@ -25,7 +26,7 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
silentDebug('[cleanup-hook] Hook fired', {
happy_path_error__with_fallback('[cleanup-hook] Hook fired', {
session_id: input?.session_id,
cwd: input?.cwd,
reason: input?.reason
@@ -58,19 +59,19 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
claudeSessionId: session_id,
reason
}),
signal: AbortSignal.timeout(2000)
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (response.ok) {
const result = await response.json();
silentDebug('[cleanup-hook] Session cleanup completed', result);
happy_path_error__with_fallback('[cleanup-hook] Session cleanup completed', result);
} else {
// Non-fatal - session might not exist
silentDebug('[cleanup-hook] Session not found or already cleaned up');
happy_path_error__with_fallback('[cleanup-hook] Session not found or already cleaned up');
}
} catch (error: any) {
// Worker might not be running - that's okay
silentDebug('[cleanup-hook] Worker not reachable (non-critical)', {
happy_path_error__with_fallback('[cleanup-hook] Worker not reachable (non-critical)', {
error: error.message
});
}
+19 -3
View File
@@ -8,8 +8,8 @@
import path from "path";
import { stdin } from "process";
import { execSync } from "child_process";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
export interface SessionStartInput {
session_id?: string;
@@ -29,8 +29,24 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
const port = getWorkerPort();
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
return result.trim();
try {
const response = await fetch(url, { signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT) });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch context: ${response.status} ${errorText}`);
}
const result = await response.text();
return result.trim();
} catch (error: any) {
// Only show restart message for connection errors, not HTTP errors
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
}
throw error;
}
}
// Entry Point - handle stdin/stdout
+28 -47
View File
@@ -35,11 +35,9 @@
import path from 'path';
import { stdin } from 'process';
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { createHookResponse } from './hook-response.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js';
import { stripMemoryTagsFromPrompt } from '../utils/tag-stripping.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
export interface UserPromptSubmitInput {
session_id: string;
@@ -63,7 +61,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const { session_id, cwd, prompt } = input;
// Debug: Log what we received
silentDebug('[new-hook] Input received', {
happy_path_error__with_fallback('[new-hook] Input received', {
session_id,
cwd,
cwd_type: typeof cwd,
@@ -74,7 +72,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const project = path.basename(cwd);
silentDebug('[new-hook] Project extracted', {
happy_path_error__with_fallback('[new-hook] Project extracted', {
project,
project_type: typeof project,
project_length: project?.length,
@@ -82,63 +80,46 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
cwd_was: cwd
});
const db = new SessionStore();
// CRITICAL: Use session_id from hook as THE source of truth
// createSDKSession is idempotent - creates new or returns existing
// This is how ALL hooks stay connected to the same session
const sessionDbId = db.createSDKSession(session_id, project, prompt);
const promptNumber = db.incrementPromptCounter(sessionDbId);
// Strip memory tags before saving user prompt to prevent privacy leaks
// Tags like <private> and <claude-mem-context> should not be stored or searchable
const cleanedUserPrompt = stripMemoryTagsFromPrompt(prompt);
// Skip memory operations for fully private prompts
// If the entire prompt was wrapped in <private> tags, don't create any observations
if (!cleanedUserPrompt || cleanedUserPrompt.trim() === '') {
silentDebug('[new-hook] Prompt entirely private, skipping memory operations', {
session_id,
promptNumber,
originalLength: prompt.length
});
db.close();
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber} (fully private - skipped)`);
console.log(createHookResponse('UserPromptSubmit', true));
return;
}
db.saveUserPrompt(session_id, promptNumber, cleanedUserPrompt);
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
db.close();
const port = getWorkerPort();
// Strip leading slash from commands for memory agent
// /review 101 → review 101 (more semantic for observations)
const cleanedPrompt = prompt.startsWith('/') ? prompt.substring(1) : prompt;
// Initialize session via HTTP - handles DB operations and privacy checks
let sessionDbId: number;
let promptNumber: number;
try {
// Initialize session via HTTP
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, userPrompt: cleanedPrompt, promptNumber }),
body: JSON.stringify({
claudeSessionId: session_id,
project,
prompt
}),
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
if (!initResponse.ok) {
const errorText = await initResponse.text();
throw new Error(`Failed to initialize session: ${initResponse.status} ${errorText}`);
}
const initResult = await initResponse.json();
sessionDbId = initResult.sessionDbId;
promptNumber = initResult.promptNumber;
// Check if prompt was entirely private (worker performs privacy check)
if (initResult.skipped && initResult.reason === 'private') {
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber} (fully private - skipped)`);
console.log(createHookResponse('UserPromptSubmit', true));
return;
}
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
} catch (error: any) {
// Only show restart message for connection errors, not HTTP errors
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
}
// Re-throw HTTP errors and other errors as-is
throw error;
}
+8 -2
View File
@@ -10,6 +10,8 @@ import { stdin } from 'process';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
export interface PostToolUseInput {
session_id: string;
@@ -65,9 +67,13 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
tool_name,
tool_input,
tool_response,
cwd: cwd || ''
cwd: happy_path_error__with_fallback(
'Missing cwd in PostToolUse hook input',
{ session_id, tool_name },
cwd || ''
)
}),
signal: AbortSignal.timeout(2000)
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (!response.ok) {
+10 -3
View File
@@ -14,6 +14,8 @@ import { readFileSync, existsSync } from 'fs';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
export interface StopInput {
session_id: string;
@@ -142,8 +144,13 @@ async function summaryHook(input?: StopInput): Promise<void> {
const port = getWorkerPort();
// Extract last user AND assistant messages from transcript
const lastUserMessage = extractLastUserMessage(input.transcript_path || '');
const lastAssistantMessage = extractLastAssistantMessage(input.transcript_path || '');
const transcriptPath = happy_path_error__with_fallback(
'Missing transcript_path in Stop hook input',
{ session_id },
input.transcript_path || ''
);
const lastUserMessage = extractLastUserMessage(transcriptPath);
const lastAssistantMessage = extractLastAssistantMessage(transcriptPath);
logger.dataIn('HOOK', 'Stop: Requesting summary', {
workerPort: port,
@@ -161,7 +168,7 @@ async function summaryHook(input?: StopInput): Promise<void> {
last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage
}),
signal: AbortSignal.timeout(2000)
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (!response.ok) {
+21 -32
View File
@@ -6,39 +6,9 @@
* has been loaded into their session. Uses stderr as the communication channel
* since it's currently the only way to display messages in Claude Code UI.
*/
import { join, basename } from "path";
import { homedir } from "os";
import { existsSync } from "fs";
import { basename } from "path";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
// Check if node_modules exists - if not, this is first run
const pluginDir = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const nodeModulesPath = join(pluginDir, 'node_modules');
if (!existsSync(nodeModulesPath)) {
// First-time installation - dependencies not yet installed
console.error(`
---
🎉 Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
user messages in Claude Code UI until a better method is provided.
---
Claude-Mem: First-Time Setup
Dependencies have been installed in the background. This only happens once.
💡 TIPS:
Memories will start generating while you work
Use /init to write or update your CLAUDE.md for better project context
Try /clear after one session to see what context looks like
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`);
process.exit(3);
}
try {
// Ensure worker is running
await ensureWorkerRunning();
@@ -111,7 +81,26 @@ try {
);
} catch (error) {
console.error(`❌ Failed to load context display: ${error}`);
// Context not available yet - likely first run or worker starting up
console.error(`
---
🎉 Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
user messages in Claude Code UI until a better method is provided.
---
Claude-Mem: First-Time Setup
Dependencies are installing in the background. This only happens once.
💡 TIPS:
Memories will start generating while you work
Use /init to write or update your CLAUDE.md for better project context
Try /clear after one session to see what context looks like
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`);
}
process.exit(3);
+7 -1
View File
@@ -3,6 +3,8 @@
* Generates prompts for the Claude Agent SDK memory worker
*/
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
export interface Observation {
id: number;
tool_name: string;
@@ -175,7 +177,11 @@ export function buildObservationPrompt(obs: Observation): string {
* Build prompt to generate progress summary
*/
export function buildSummaryPrompt(session: SDKSession): string {
const lastAssistantMessage = session.last_assistant_message || '';
const lastAssistantMessage = happy_path_error__with_fallback(
'Missing last_assistant_message in session for summary prompt',
session,
session.last_assistant_message || ''
);
return `PROGRESS SUMMARY CHECKPOINT
===========================
+13 -12
View File
@@ -14,12 +14,13 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { silentDebug } from '../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { getWorkerPort } from '../shared/worker-utils.js';
/**
* Worker HTTP API configuration
*/
const WORKER_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
const WORKER_PORT = getWorkerPort();
const WORKER_BASE_URL = `http://localhost:${WORKER_PORT}`;
/**
@@ -49,7 +50,7 @@ async function callWorkerAPI(
endpoint: string,
params: Record<string, any>
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
silentDebug('[mcp-server] → Worker API', { endpoint, params });
happy_path_error__with_fallback('[mcp-server] → Worker API', { endpoint, params });
try {
const searchParams = new URLSearchParams();
@@ -71,12 +72,12 @@ async function callWorkerAPI(
const data = await response.json() as { content: Array<{ type: 'text'; text: string }>; isError?: boolean };
silentDebug('[mcp-server] ← Worker API success', { endpoint });
happy_path_error__with_fallback('[mcp-server] ← Worker API success', { endpoint });
// Worker returns { content: [...] } format directly
return data;
} catch (error: any) {
silentDebug('[mcp-server] ← Worker API error', { endpoint, error: error.message });
happy_path_error__with_fallback('[mcp-server] ← Worker API error', { endpoint, error: error.message });
return {
content: [{
type: 'text' as const,
@@ -411,7 +412,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Cleanup function
async function cleanup() {
silentDebug('[mcp-server] Shutting down...');
happy_path_error__with_fallback('[mcp-server] Shutting down...');
process.exit(0);
}
@@ -424,22 +425,22 @@ async function main() {
// Start the MCP server
const transport = new StdioServerTransport();
await server.connect(transport);
silentDebug('[mcp-server] Claude-mem search server started');
happy_path_error__with_fallback('[mcp-server] Claude-mem search server started');
// Check Worker availability in background
setTimeout(async () => {
const workerAvailable = await verifyWorkerConnection();
if (!workerAvailable) {
silentDebug('[mcp-server] WARNING: Worker not available at', WORKER_BASE_URL);
silentDebug('[mcp-server] Tools will fail until Worker is started');
silentDebug('[mcp-server] Start Worker with: npm run worker:restart');
happy_path_error__with_fallback('[mcp-server] WARNING: Worker not available at', WORKER_BASE_URL);
happy_path_error__with_fallback('[mcp-server] Tools will fail until Worker is started');
happy_path_error__with_fallback('[mcp-server] Start Worker with: npm run worker:restart');
} else {
silentDebug('[mcp-server] Worker available at', WORKER_BASE_URL);
happy_path_error__with_fallback('[mcp-server] Worker available at', WORKER_BASE_URL);
}
}, 0);
}
main().catch((error) => {
silentDebug('[mcp-server] Fatal error:', error);
happy_path_error__with_fallback('[mcp-server] Fatal error:', error);
process.exit(1);
});
+1 -1
View File
@@ -16,7 +16,7 @@ import {
TYPE_WORK_EMOJI_MAP
} from '../constants/observation-metadata.js';
import { logger } from '../utils/logger.js';
import { SettingsDefaultsManager } from './worker/settings/SettingsDefaultsManager.js';
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
// Version marker path - use homedir-based path that works in both CJS and ESM contexts
const VERSION_MARKER_PATH = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', '.install-version');
+10 -2
View File
@@ -13,6 +13,9 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { ParsedObservation, ParsedSummary } from '../../sdk/parser.js';
import { SessionStore } from '../sqlite/SessionStore.js';
import { logger } from '../../utils/logger.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import path from 'path';
import os from 'os';
@@ -96,7 +99,8 @@ export class ChromaSync {
try {
// Use Python 3.13 by default to avoid onnxruntime compatibility issues with Python 3.14+
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
const pythonVersion = process.env.CLAUDE_MEM_PYTHON_VERSION || '3.13';
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
const transport = new StdioClientTransport({
command: 'uvx',
args: [
@@ -763,7 +767,11 @@ export class ChromaSync {
arguments: arguments_obj
});
const resultText = result.content[0]?.text || '';
const resultText = happy_path_error__with_fallback(
'Missing text in MCP chroma_query_documents result',
{ project: this.project, query_text: query },
result.content[0]?.text || ''
);
// Parse JSON response
let parsed: any;
+22 -12
View File
@@ -14,10 +14,11 @@ import path from 'path';
import { DatabaseManager } from './DatabaseManager.js';
import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { silentDebug } from '../../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import { parseObservations, parseSummary } from '../../sdk/parser.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from './settings/SettingsDefaultsManager.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
// Import Agent SDK (assumes it's installed)
@@ -232,8 +233,16 @@ export class SDKAgent {
sdk_session_id: session.sdkSessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
last_user_message: happy_path_error__with_fallback(
'Missing last_user_message for summary in SDKAgent',
{ sessionDbId: session.sessionDbId, sdkSessionId: session.sdkSessionId },
message.last_user_message || ''
),
last_assistant_message: happy_path_error__with_fallback(
'Missing last_assistant_message for summary in SDKAgent',
{ sessionDbId: session.sessionDbId, sdkSessionId: session.sdkSessionId },
message.last_assistant_message || ''
)
})
},
session_id: session.claudeSessionId,
@@ -267,16 +276,16 @@ export class SDKAgent {
sessionId: session.sessionDbId,
obsId,
type: obs.type,
title: obs.title || silentDebug('obs.title is null', { obsId, type: obs.type }, '(untitled)'),
filesRead: obs.files_read?.length ?? (silentDebug('obs.files_read is null/undefined', { obsId }), 0),
filesModified: obs.files_modified?.length ?? (silentDebug('obs.files_modified is null/undefined', { obsId }), 0),
concepts: obs.concepts?.length ?? (silentDebug('obs.concepts is null/undefined', { obsId }), 0)
title: obs.title || happy_path_error__with_fallback('obs.title is null', { obsId, type: obs.type }, '(untitled)'),
filesRead: obs.files_read?.length ?? (happy_path_error__with_fallback('obs.files_read is null/undefined', { obsId }), 0),
filesModified: obs.files_modified?.length ?? (happy_path_error__with_fallback('obs.files_modified is null/undefined', { obsId }), 0),
concepts: obs.concepts?.length ?? (happy_path_error__with_fallback('obs.concepts is null/undefined', { obsId }), 0)
});
// Sync to Chroma with error logging
const chromaStart = Date.now();
const obsType = obs.type;
const obsTitle = obs.title || silentDebug('obs.title is null for Chroma sync', { obsId, type: obs.type }, '(untitled)');
const obsTitle = obs.title || happy_path_error__with_fallback('obs.title is null for Chroma sync', { obsId, type: obs.type }, '(untitled)');
this.dbManager.getChromaSync().syncObservation(
obsId,
session.claudeSessionId,
@@ -344,14 +353,14 @@ export class SDKAgent {
logger.info('SDK', 'Summary saved', {
sessionId: session.sessionDbId,
summaryId,
request: summary.request || silentDebug('summary.request is null', { summaryId }, '(no request)'),
request: summary.request || happy_path_error__with_fallback('summary.request is null', { summaryId }, '(no request)'),
hasCompleted: !!summary.completed,
hasNextSteps: !!summary.next_steps
});
// Sync to Chroma with error logging
const chromaStart = Date.now();
const summaryRequest = summary.request || silentDebug('summary.request is null for Chroma sync', { summaryId }, '(no request)');
const summaryRequest = summary.request || happy_path_error__with_fallback('summary.request is null for Chroma sync', { summaryId }, '(no request)');
this.dbManager.getChromaSync().syncSummary(
summaryId,
session.claudeSessionId,
@@ -410,7 +419,8 @@ export class SDKAgent {
* Find Claude executable (inline, called once per session)
*/
private findClaudeExecutable(): string {
const claudePath = process.env.CLAUDE_CODE_PATH ||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
const claudePath = settings.CLAUDE_CODE_PATH ||
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8', windowsHide: true })
.trim().split('\n')[0].trim();
+59 -59
View File
@@ -13,7 +13,7 @@ import { ChromaSync } from '../sync/ChromaSync.js';
import { FormattingService } from './FormattingService.js';
import { TimelineService, TimelineItem } from './TimelineService.js';
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
import { silentDebug } from '../../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
const COLLECTION_NAME = 'cm__claude-mem';
@@ -97,7 +97,7 @@ export class SearchManager {
// PATH 1: FILTER-ONLY (no query text) - Skip Chroma/FTS5, use direct SQLite filtering
// This path enables date filtering which Chroma cannot do (requires direct SQLite access)
if (!query) {
silentDebug(`[mcp-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
happy_path_error__with_fallback(`[mcp-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
const obsOptions = { ...options, type: obs_type, concepts, files };
if (searchObservations) {
observations = this.sessionSearch.searchObservations(undefined, obsOptions);
@@ -113,7 +113,7 @@ export class SearchManager {
else if (this.chromaSync) {
let chromaSucceeded = false;
try {
silentDebug(`[mcp-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
happy_path_error__with_fallback(`[mcp-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
// Build Chroma where filter for doc_type
let whereFilter: Record<string, any> | undefined;
@@ -128,7 +128,7 @@ export class SearchManager {
// Step 1: Chroma semantic search with optional type filter
const chromaResults = await this.queryChroma(query, 100, whereFilter);
chromaSucceeded = true; // Chroma didn't throw error
silentDebug(`[mcp-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
happy_path_error__with_fallback(`[mcp-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -139,7 +139,7 @@ export class SearchManager {
isRecent: meta && meta.created_at_epoch > ninetyDaysAgo
})).filter(item => item.isRecent);
silentDebug(`[mcp-server] ${recentMetadata.length} results within 90-day window`);
happy_path_error__with_fallback(`[mcp-server] ${recentMetadata.length} results within 90-day window`);
// Step 3: Categorize IDs by document type
const obsIds: number[] = [];
@@ -157,7 +157,7 @@ export class SearchManager {
}
}
silentDebug(`[mcp-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
happy_path_error__with_fallback(`[mcp-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
// Step 4: Hydrate from SQLite with additional filters
if (obsIds.length > 0) {
@@ -172,14 +172,14 @@ export class SearchManager {
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit });
}
silentDebug(`[mcp-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
happy_path_error__with_fallback(`[mcp-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
} else {
// Chroma returned 0 results - this is the correct answer, don't fall back to FTS5
silentDebug(`[mcp-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
happy_path_error__with_fallback(`[mcp-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
}
} catch (chromaError: any) {
silentDebug('[mcp-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message);
silentDebug('[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
happy_path_error__with_fallback('[mcp-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
// Return empty results - no fallback
observations = [];
sessions = [];
@@ -188,8 +188,8 @@ export class SearchManager {
}
// ChromaDB not initialized - return empty results (no fallback)
else {
silentDebug(`[mcp-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`);
silentDebug(`[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
happy_path_error__with_fallback(`[mcp-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`);
happy_path_error__with_fallback(`[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
observations = [];
sessions = [];
prompts = [];
@@ -312,9 +312,9 @@ export class SearchManager {
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using hybrid semantic search for timeline query');
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for timeline query');
const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[mcp-server] Chroma returned ${chromaResults?.ids?.length ?? 0} semantic matches`);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults?.ids?.length ?? 0} semantic matches`);
if (chromaResults?.ids && chromaResults.ids.length > 0) {
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
@@ -328,7 +328,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -345,7 +345,7 @@ export class SearchManager {
const topResult = results[0];
anchorId = topResult.id;
anchorEpoch = topResult.created_at_epoch;
silentDebug(`[mcp-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
happy_path_error__with_fallback(`[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);
}
// MODE 2: Anchor-based timeline
@@ -621,7 +621,7 @@ export class SearchManager {
try {
if (query) {
// Semantic search filtered to decision type
silentDebug('[mcp-server] Using Chroma semantic search with type=decision filter');
happy_path_error__with_fallback('[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 obsIds = chromaResults.ids;
@@ -632,7 +632,7 @@ export class SearchManager {
}
} else {
// No query: get all decisions, rank by "decision" keyword
silentDebug('[mcp-server] Using metadata-first + semantic ranking for decisions');
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for decisions');
const metadataResults = this.sessionSearch.findByType('decision', filters);
if (metadataResults.length > 0) {
@@ -653,7 +653,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma search failed, using SQLite fallback:', chromaError.message);
happy_path_error__with_fallback('[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
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using hybrid search for change-related observations');
happy_path_error__with_fallback('[mcp-server] Using hybrid search for change-related observations');
// Get all observations with type="change" or concepts containing change
const typeResults = this.sessionSearch.findByType('change', filters);
@@ -737,7 +737,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
}
}
@@ -807,7 +807,7 @@ export class SearchManager {
// Search for how-it-works concept observations
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using metadata-first + semantic ranking for how-it-works');
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for how-it-works');
const metadataResults = this.sessionSearch.findByConcept('how-it-works', filters);
if (metadataResults.length > 0) {
@@ -827,7 +827,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
}
}
@@ -883,11 +883,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using hybrid semantic search (Chroma + SQLite)');
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search (Chroma + SQLite)');
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -897,17 +897,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -960,11 +960,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using hybrid semantic search for sessions');
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for sessions');
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'session_summary' });
silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -974,17 +974,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[mcp-server] Hydrated ${results.length} sessions from SQLite`);
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} sessions from SQLite`);
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -1037,11 +1037,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using hybrid semantic search for user prompts');
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for user prompts');
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'user_prompt' });
silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -1051,17 +1051,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit });
silentDebug(`[mcp-server] Hydrated ${results.length} user prompts from SQLite`);
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} user prompts from SQLite`);
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -1114,11 +1114,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using metadata-first + semantic ranking for concept search');
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for concept search');
// Step 1: SQLite metadata filter (get all IDs with this concept)
const metadataResults = this.sessionSearch.findByConcept(concept, filters);
silentDebug(`[mcp-server] Found ${metadataResults.length} observations with concept "${concept}"`);
happy_path_error__with_fallback(`[mcp-server] Found ${metadataResults.length} observations with concept "${concept}"`);
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to concept)
@@ -1133,7 +1133,7 @@ export class SearchManager {
}
}
silentDebug(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
happy_path_error__with_fallback(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1143,14 +1143,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
silentDebug('[mcp-server] Using SQLite-only concept search');
happy_path_error__with_fallback('[mcp-server] Using SQLite-only concept search');
results = this.sessionSearch.findByConcept(concept, filters);
}
@@ -1204,11 +1204,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search for observations
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using metadata-first + semantic ranking for file search');
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for file search');
// Step 1: SQLite metadata filter (get all results with this file)
const metadataResults = this.sessionSearch.findByFile(filePath, filters);
silentDebug(`[mcp-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`);
happy_path_error__with_fallback(`[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 = metadataResults.sessions;
@@ -1227,7 +1227,7 @@ export class SearchManager {
}
}
silentDebug(`[mcp-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
happy_path_error__with_fallback(`[mcp-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1237,14 +1237,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (observations.length === 0 && sessions.length === 0) {
silentDebug('[mcp-server] Using SQLite-only file search');
happy_path_error__with_fallback('[mcp-server] Using SQLite-only file search');
const results = this.sessionSearch.findByFile(filePath, filters);
observations = results.observations;
sessions = results.sessions;
@@ -1323,11 +1323,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using metadata-first + semantic ranking for type search');
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for type search');
// Step 1: SQLite metadata filter (get all IDs with this type)
const metadataResults = this.sessionSearch.findByType(type, filters);
silentDebug(`[mcp-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
happy_path_error__with_fallback(`[mcp-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to type)
@@ -1342,7 +1342,7 @@ export class SearchManager {
}
}
silentDebug(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
happy_path_error__with_fallback(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1352,14 +1352,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
silentDebug('[mcp-server] Using SQLite-only type search');
happy_path_error__with_fallback('[mcp-server] Using SQLite-only type search');
results = this.sessionSearch.findByType(type, filters);
}
@@ -1815,9 +1815,9 @@ export class SearchManager {
// Use hybrid search if available
if (this.chromaSync) {
try {
silentDebug('[mcp-server] Using hybrid semantic search for timeline query');
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for timeline query');
const chromaResults = await this.queryChroma(query, 100);
silentDebug(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Filter by recency (90 days)
@@ -1827,15 +1827,15 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
silentDebug(`[mcp-server] ${recentIds.length} results within 90-day window`);
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
if (recentIds.length > 0) {
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit: mode === 'auto' ? 1 : limit });
silentDebug(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
}
}
} catch (chromaError: any) {
silentDebug('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -1886,7 +1886,7 @@ export class SearchManager {
} else {
// Auto mode: Use top result as timeline anchor
const topResult = results[0];
silentDebug(`[mcp-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
happy_path_error__with_fallback(`[mcp-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
// Get timeline around this observation
const timelineData = this.sessionStore.getTimelineAroundObservation(
+6 -6
View File
@@ -11,7 +11,7 @@
import { EventEmitter } from 'events';
import { DatabaseManager } from './DatabaseManager.js';
import { logger } from '../../utils/logger.js';
import { silentDebug } from '../../utils/silent-debug.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import type { ActiveSession, PendingMessage, ObservationData } from '../worker-types.js';
export class SessionManager {
@@ -43,7 +43,7 @@ export class SessionManager {
// in the database but the in-memory session still has the stale empty value
const dbSession = this.dbManager.getSessionById(sessionDbId);
if (dbSession.project && dbSession.project !== session.project) {
silentDebug('[SessionManager] Updating project from database', {
happy_path_error__with_fallback('[SessionManager] Updating project from database', {
sessionDbId,
oldProject: session.project,
newProject: dbSession.project
@@ -53,7 +53,7 @@ export class SessionManager {
// Update userPrompt for continuation prompts
if (currentUserPrompt) {
silentDebug('[SessionManager] Updating userPrompt for continuation', {
happy_path_error__with_fallback('[SessionManager] Updating userPrompt for continuation', {
sessionDbId,
promptNumber,
oldPrompt: session.userPrompt.substring(0, 80),
@@ -62,7 +62,7 @@ export class SessionManager {
session.userPrompt = currentUserPrompt;
session.lastPromptNumber = promptNumber || session.lastPromptNumber;
} else {
silentDebug('[SessionManager] No currentUserPrompt provided for existing session', {
happy_path_error__with_fallback('[SessionManager] No currentUserPrompt provided for existing session', {
sessionDbId,
promptNumber,
usingCachedPrompt: session.userPrompt.substring(0, 80)
@@ -78,13 +78,13 @@ export class SessionManager {
const userPrompt = currentUserPrompt || dbSession.user_prompt;
if (!currentUserPrompt) {
silentDebug('[SessionManager] No currentUserPrompt provided for new session, using database', {
happy_path_error__with_fallback('[SessionManager] No currentUserPrompt provided for new session, using database', {
sessionDbId,
promptNumber,
dbPrompt: dbSession.user_prompt.substring(0, 80)
});
} else {
silentDebug('[SessionManager] Initializing session with fresh userPrompt', {
happy_path_error__with_fallback('[SessionManager] Initializing session with fresh userPrompt', {
sessionDbId,
promptNumber,
userPrompt: currentUserPrompt.substring(0, 80)
@@ -8,7 +8,8 @@
import express, { Request, Response } from 'express';
import { getWorkerPort } from '../../../../shared/worker-utils.js';
import { logger } from '../../../../utils/logger.js';
import { stripMemoryTagsFromJson } from '../../../../utils/tag-stripping.js';
import { stripMemoryTagsFromJson, stripMemoryTagsFromPrompt } from '../../../../utils/tag-stripping.js';
import { happy_path_error__with_fallback } from '../../../../utils/silent-debug.js';
import { SessionManager } from '../../SessionManager.js';
import { DatabaseManager } from '../../DatabaseManager.js';
import { SDKAgent } from '../../SDKAgent.js';
@@ -70,6 +71,7 @@ export class SessionRoutes extends BaseRouteHandler {
app.post('/sessions/:sessionDbId/complete', this.handleSessionComplete.bind(this));
// New session endpoints (use claudeSessionId)
app.post('/api/sessions/init', this.handleSessionInitByClaudeId.bind(this));
app.post('/api/sessions/observations', this.handleObservationsByClaudeId.bind(this));
app.post('/api/sessions/summarize', this.handleSummarizeByClaudeId.bind(this));
app.post('/api/sessions/complete', this.handleSessionCompleteByClaudeId.bind(this));
@@ -289,6 +291,7 @@ export class SessionRoutes extends BaseRouteHandler {
? stripMemoryTagsFromJson(JSON.stringify(tool_input))
: '{}';
} catch (error) {
logger.debug('SESSION', 'Failed to serialize tool_input', { sessionDbId }, error);
cleanedToolInput = '{"error": "Failed to serialize tool_input"}';
}
@@ -297,6 +300,7 @@ export class SessionRoutes extends BaseRouteHandler {
? stripMemoryTagsFromJson(JSON.stringify(tool_response))
: '{}';
} catch (error) {
logger.debug('SESSION', 'Failed to serialize tool_result', { sessionDbId }, error);
cleanedToolResponse = '{"error": "Failed to serialize tool_response"}';
}
@@ -306,7 +310,11 @@ export class SessionRoutes extends BaseRouteHandler {
tool_input: cleanedToolInput,
tool_response: cleanedToolResponse,
prompt_number: promptNumber,
cwd: cwd || ''
cwd: happy_path_error__with_fallback(
'Missing cwd when queueing observation in SessionRoutes',
{ sessionDbId, tool_name },
cwd || ''
)
});
// Ensure SDK agent is running
@@ -352,7 +360,15 @@ export class SessionRoutes extends BaseRouteHandler {
}
// Queue summarize
this.sessionManager.queueSummarize(sessionDbId, last_user_message || '', last_assistant_message);
this.sessionManager.queueSummarize(
sessionDbId,
happy_path_error__with_fallback(
'Missing last_user_message when queueing summary in SessionRoutes',
{ sessionDbId },
last_user_message || ''
),
last_assistant_message
);
// Ensure SDK agent is running
this.ensureGeneratorRunning(sessionDbId, 'summarize');
@@ -387,4 +403,68 @@ export class SessionRoutes extends BaseRouteHandler {
res.json({ success: true });
});
/**
* Initialize session by claudeSessionId (new-hook uses this)
* POST /api/sessions/init
* Body: { claudeSessionId, project, prompt }
*
* Performs all session initialization DB operations:
* - Creates/gets SDK session (idempotent)
* - Increments prompt counter
* - Saves user prompt (with privacy tag stripping)
*
* Returns: { sessionDbId, promptNumber, skipped: boolean, reason?: string }
*/
private handleSessionInitByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { claudeSessionId, project, prompt } = req.body;
// Validate required parameters
if (!this.validateRequired(req, res, ['claudeSessionId', 'project', 'prompt'])) {
return;
}
const store = this.dbManager.getSessionStore();
// Step 1: Create/get SDK session (idempotent INSERT OR IGNORE)
const sessionDbId = store.createSDKSession(claudeSessionId, project, prompt);
// Step 2: Increment prompt counter
const promptNumber = store.incrementPromptCounter(sessionDbId);
// Step 3: Strip privacy tags from prompt
const cleanedPrompt = stripMemoryTagsFromPrompt(prompt);
// Step 4: Check if prompt is entirely private
if (!cleanedPrompt || cleanedPrompt.trim() === '') {
logger.debug('HOOK', 'Session init - prompt entirely private', {
sessionId: sessionDbId,
promptNumber,
originalLength: prompt.length
});
res.json({
sessionDbId,
promptNumber,
skipped: true,
reason: 'private'
});
return;
}
// Step 5: Save cleaned user prompt
store.saveUserPrompt(claudeSessionId, promptNumber, cleanedPrompt);
logger.info('SESSION', 'Session initialized via HTTP', {
sessionId: sessionDbId,
promptNumber,
project
});
res.json({
sessionDbId,
promptNumber,
skipped: false
});
});
}
@@ -7,7 +7,7 @@
import express, { Request, Response } from 'express';
import path from 'path';
import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
import { readFileSync, writeFileSync, existsSync, renameSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { getPackageRoot } from '../../../../shared/paths.js';
import { logger } from '../../../../utils/logger.js';
@@ -20,7 +20,7 @@ import {
ObservationConcept
} from '../../../../constants/observation-metadata.js';
import { BaseRouteHandler } from '../BaseRouteHandler.js';
import { SettingsDefaultsManager } from '../../settings/SettingsDefaultsManager.js';
import { SettingsDefaultsManager } from '../../../../shared/SettingsDefaultsManager.js';
export class SettingsRoutes extends BaseRouteHandler {
constructor(
@@ -45,16 +45,17 @@ export class SettingsRoutes extends BaseRouteHandler {
}
/**
* Get environment settings (from ~/.claude/settings.json)
* Get environment settings (from ~/.claude-mem/settings.json)
*/
private handleGetSettings = this.wrapHandler((req: Request, res: Response): void => {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
this.ensureSettingsFile(settingsPath);
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
res.json(settings);
});
/**
* Update environment settings (in ~/.claude/settings.json) with validation
* Update environment settings (in ~/.claude-mem/settings.json) with validation
*/
private handleUpdateSettings = this.wrapHandler((req: Request, res: Response): void => {
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
@@ -81,6 +82,30 @@ export class SettingsRoutes extends BaseRouteHandler {
}
}
// Validate CLAUDE_MEM_LOG_LEVEL
if (req.body.CLAUDE_MEM_LOG_LEVEL) {
const validLevels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'SILENT'];
if (!validLevels.includes(req.body.CLAUDE_MEM_LOG_LEVEL.toUpperCase())) {
res.status(400).json({
success: false,
error: 'CLAUDE_MEM_LOG_LEVEL must be one of: DEBUG, INFO, WARN, ERROR, SILENT'
});
return;
}
}
// Validate CLAUDE_MEM_PYTHON_VERSION (must be valid Python version format)
if (req.body.CLAUDE_MEM_PYTHON_VERSION) {
const pythonVersionRegex = /^3\.\d{1,2}$/;
if (!pythonVersionRegex.test(req.body.CLAUDE_MEM_PYTHON_VERSION)) {
res.status(400).json({
success: false,
error: 'CLAUDE_MEM_PYTHON_VERSION must be in format "3.X" or "3.XX" (e.g., "3.13")'
});
return;
}
}
// Validate context settings
const validation = this.validateContextSettings(req.body);
if (!validation.valid) {
@@ -93,14 +118,12 @@ export class SettingsRoutes extends BaseRouteHandler {
// Read existing settings
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
let settings: any = { env: {} };
this.ensureSettingsFile(settingsPath);
let settings: any = {};
if (existsSync(settingsPath)) {
const settingsData = readFileSync(settingsPath, 'utf-8');
settings = JSON.parse(settingsData);
if (!settings.env) {
settings.env = {};
}
}
// Update all settings from request body
@@ -108,22 +131,31 @@ export class SettingsRoutes extends BaseRouteHandler {
'CLAUDE_MEM_MODEL',
'CLAUDE_MEM_CONTEXT_OBSERVATIONS',
'CLAUDE_MEM_WORKER_PORT',
// System Configuration
'CLAUDE_MEM_DATA_DIR',
'CLAUDE_MEM_LOG_LEVEL',
'CLAUDE_MEM_PYTHON_VERSION',
'CLAUDE_CODE_PATH',
// Token Economics
'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS',
'CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS',
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT',
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT',
// Observation Filtering
'CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES',
'CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS',
// Display Configuration
'CLAUDE_MEM_CONTEXT_FULL_COUNT',
'CLAUDE_MEM_CONTEXT_FULL_FIELD',
'CLAUDE_MEM_CONTEXT_SESSION_COUNT',
// Feature Toggles
'CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE',
];
for (const key of settingKeys) {
if (req.body[key] !== undefined) {
settings.env[key] = req.body[key];
settings[key] = req.body[key];
}
}
@@ -322,4 +354,22 @@ export class SettingsRoutes extends BaseRouteHandler {
throw error;
}
}
/**
* Ensure settings file exists, creating with defaults if missing
*/
private ensureSettingsFile(settingsPath: string): void {
if (!existsSync(settingsPath)) {
const defaults = SettingsDefaultsManager.getAllDefaults();
// Ensure directory exists
const dir = path.dirname(settingsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(settingsPath, JSON.stringify(defaults, null, 2), 'utf-8');
logger.info('SETTINGS', 'Created settings file with defaults', { settingsPath });
}
}
}
@@ -5,13 +5,21 @@
* Provides methods to get defaults with optional environment variable overrides.
*/
import { readFileSync, existsSync } from 'fs';
import { DEFAULT_OBSERVATION_TYPES_STRING, DEFAULT_OBSERVATION_CONCEPTS_STRING } from '../../../constants/observation-metadata.js';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { DEFAULT_OBSERVATION_TYPES_STRING, DEFAULT_OBSERVATION_CONCEPTS_STRING } from '../constants/observation-metadata.js';
import { logger } from '../utils/logger.js';
export interface SettingsDefaults {
CLAUDE_MEM_MODEL: string;
CLAUDE_MEM_CONTEXT_OBSERVATIONS: string;
CLAUDE_MEM_WORKER_PORT: string;
// System Configuration
CLAUDE_MEM_DATA_DIR: string;
CLAUDE_MEM_LOG_LEVEL: string;
CLAUDE_MEM_PYTHON_VERSION: string;
CLAUDE_CODE_PATH: string;
// Token Economics
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: string;
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: string;
@@ -37,6 +45,11 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_MODEL: 'claude-haiku-4-5',
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
CLAUDE_MEM_WORKER_PORT: '37777',
// System Configuration
CLAUDE_MEM_DATA_DIR: join(homedir(), '.claude-mem'),
CLAUDE_MEM_LOG_LEVEL: 'INFO',
CLAUDE_MEM_PYTHON_VERSION: '3.13',
CLAUDE_CODE_PATH: '', // Empty means auto-detect via 'which claude'
// Token Economics
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'true',
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'true',
@@ -62,14 +75,14 @@ export class SettingsDefaultsManager {
}
/**
* Get a default value with optional environment variable override
* Get a default value from defaults (no environment variable override)
*/
static get(key: keyof SettingsDefaults): string {
return process.env[key] || this.DEFAULTS[key];
return this.DEFAULTS[key];
}
/**
* Get an integer default value with optional environment variable override
* Get an integer default value
*/
static getInt(key: keyof SettingsDefaults): number {
const value = this.get(key);
@@ -77,7 +90,7 @@ export class SettingsDefaultsManager {
}
/**
* Get a boolean default value with optional environment variable override
* Get a boolean default value
*/
static getBool(key: keyof SettingsDefaults): boolean {
const value = this.get(key);
@@ -95,13 +108,28 @@ export class SettingsDefaultsManager {
const settingsData = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(settingsData);
const env = settings.env || {};
// Merge file settings with defaults
// MIGRATION: Handle old nested schema { env: {...} }
let flatSettings = settings;
if (settings.env && typeof settings.env === 'object') {
// Migrate from nested to flat schema
flatSettings = settings.env;
// Auto-migrate the file to flat schema
try {
writeFileSync(settingsPath, JSON.stringify(flatSettings, null, 2), 'utf-8');
logger.info('SETTINGS', 'Migrated settings file from nested to flat schema', { settingsPath });
} catch (error) {
logger.warn('SETTINGS', 'Failed to auto-migrate settings file', { settingsPath }, error);
// Continue with in-memory migration even if write fails
}
}
// Merge file settings with defaults (flat schema)
const result: SettingsDefaults = { ...this.DEFAULTS };
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
if (env[key] !== undefined) {
result[key] = env[key];
if (flatSettings[key] !== undefined) {
result[key] = flatSettings[key];
}
}
+13
View File
@@ -0,0 +1,13 @@
export const HOOK_TIMEOUTS = {
DEFAULT: 5000, // Standard HTTP timeout (up from 2000ms)
HEALTH_CHECK: 1000, // Worker health check (up from 500ms)
WORKER_STARTUP_WAIT: 1000,
WORKER_STARTUP_RETRIES: 15,
WINDOWS_MULTIPLIER: 1.5 // Platform-specific adjustment
} as const;
export function getTimeout(baseTimeout: number): number {
return process.platform === 'win32'
? Math.round(baseTimeout * HOOK_TIMEOUTS.WINDOWS_MULTIPLIER)
: baseTimeout;
}
+4 -8
View File
@@ -3,6 +3,7 @@ import { homedir } from 'os';
import { existsSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { SettingsDefaultsManager } from './SettingsDefaultsManager.js';
// Get __dirname that works in both ESM (hooks) and CJS (worker) contexts
function getDirname(): string {
@@ -22,7 +23,8 @@ const _dirname = getDirname();
*/
// Base directories
export const DATA_DIR = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
export const DATA_DIR = SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR');
// Note: CLAUDE_CONFIG_DIR is a Claude Code setting, not claude-mem, so leave as env var
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
// Data subdirectories
@@ -110,13 +112,7 @@ export function getPackageRoot(): string {
*/
export function getPackageCommandsDir(): string {
const packageRoot = getPackageRoot();
const commandsDir = join(packageRoot, 'commands');
if (!existsSync(join(commandsDir, 'save.md'))) {
throw new Error('Package commands directory missing required files');
}
return commandsDir;
return join(packageRoot, 'commands');
}
/**
+44 -8
View File
@@ -2,7 +2,9 @@ import path from "path";
import { existsSync } from "fs";
import { homedir } from "os";
import { spawnSync } from "child_process";
import { SettingsDefaultsManager } from "../services/worker/settings/SettingsDefaultsManager.js";
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
import { logger } from "../utils/logger.js";
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
// CRITICAL: Always use marketplace directory for PM2/ecosystem
// This ensures cross-platform compatibility and avoids cache directory confusion
@@ -10,9 +12,9 @@ const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplace
// Named constants for health checks
// Windows needs longer timeouts due to startup overhead
const HEALTH_CHECK_TIMEOUT_MS = 500;
const WORKER_STARTUP_WAIT_MS = 1000;
const WORKER_STARTUP_RETRIES = 15;
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
const WORKER_STARTUP_WAIT_MS = HOOK_TIMEOUTS.WORKER_STARTUP_WAIT;
const WORKER_STARTUP_RETRIES = HOOK_TIMEOUTS.WORKER_STARTUP_RETRIES;
/**
* Get the worker port number
@@ -34,7 +36,11 @@ async function isWorkerHealthy(): Promise<boolean> {
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
return response.ok;
} catch {
} catch (error) {
logger.debug('SYSTEM', 'Worker health check failed', {
error: error instanceof Error ? error.message : String(error),
errorType: error?.constructor?.name
});
return false;
}
}
@@ -55,11 +61,15 @@ async function startWorker(): Promise<boolean> {
if (process.platform === 'win32') {
// On Windows, use PowerShell Start-Process with -WindowStyle Hidden
// This avoids visible console windows that PM2 creates on Windows
// Escape single quotes for PowerShell by doubling them
const escapedScript = workerScript.replace(/'/g, "''");
const escapedWorkingDir = MARKETPLACE_ROOT.replace(/'/g, "''");
const result = spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process -FilePath 'node' -ArgumentList '${workerScript}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden`
`Start-Process -FilePath 'node' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkingDir}' -WindowStyle Hidden`
], {
cwd: MARKETPLACE_ROOT,
stdio: 'pipe',
@@ -79,7 +89,28 @@ async function startWorker(): Promise<boolean> {
}
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
const pm2Command = existsSync(localPm2Base) ? localPm2Base : 'pm2';
let pm2Command: string;
if (existsSync(localPm2Base)) {
pm2Command = localPm2Base;
} else {
// Check if global pm2 exists
const globalPm2Check = spawnSync('which', ['pm2'], {
encoding: 'utf-8',
stdio: 'pipe'
});
if (globalPm2Check.status !== 0) {
throw new Error(
'PM2 not found. Install it locally with:\n' +
` cd ${MARKETPLACE_ROOT}\n` +
' npm install\n\n' +
'Or install globally with: npm install -g pm2'
);
}
pm2Command = 'pm2';
}
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: MARKETPLACE_ROOT,
@@ -102,7 +133,12 @@ async function startWorker(): Promise<boolean> {
return false;
} catch (error) {
// Failed to start worker
logger.error('SYSTEM', 'Failed to start worker', {
platform: process.platform,
workerScript: path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'),
error: error instanceof Error ? error.message : String(error),
marketplaceRoot: MARKETPLACE_ROOT
});
return false;
}
}
+17 -8
View File
@@ -3,6 +3,8 @@
* Provides readable, traceable logging with correlation IDs and data flow tracking
*/
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
export enum LogLevel {
DEBUG = 0,
INFO = 1,
@@ -21,18 +23,25 @@ interface LogContext {
}
class Logger {
private level: LogLevel;
private level: LogLevel | null = null;
private useColor: boolean;
constructor() {
// Parse log level from environment
const envLevel = process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase() || 'INFO';
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
// Disable colors when output is not a TTY (e.g., PM2 logs)
this.useColor = process.stdout.isTTY ?? false;
}
/**
* Lazy-load log level from settings (breaks circular dependency with SettingsDefaultsManager)
*/
private getLevel(): LogLevel {
if (this.level === null) {
const envLevel = SettingsDefaultsManager.get('CLAUDE_MEM_LOG_LEVEL').toUpperCase();
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
}
return this.level;
}
/**
* Create correlation ID for tracking an observation through the pipeline
*/
@@ -60,7 +69,7 @@ class Logger {
if (typeof data === 'object') {
// If it's an error, show message and stack in debug mode
if (data instanceof Error) {
return this.level === LogLevel.DEBUG
return this.getLevel() === LogLevel.DEBUG
? `${data.message}\n${data.stack}`
: data.message;
}
@@ -132,7 +141,7 @@ class Logger {
context?: LogContext,
data?: any
): void {
if (level < this.level) return;
if (level < this.getLevel()) return;
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
const levelStr = LogLevel[level].padEnd(5);
@@ -149,7 +158,7 @@ class Logger {
// Build data part
let dataStr = '';
if (data !== undefined && data !== null) {
if (this.level === LogLevel.DEBUG && typeof data === 'object') {
if (this.getLevel() === LogLevel.DEBUG && typeof data === 'object') {
// In debug mode, show full JSON for objects
dataStr = '\n' + JSON.stringify(data, null, 2);
} else {
+18 -10
View File
@@ -1,25 +1,27 @@
/**
* Silent Debug Logger
* Happy Path Error With Fallback
*
* Semantic meaning: "When the happy path fails, this is an error, but we have a fallback."
*
* NOTE: This utility is to be used like Frank's Red Hot, we put that shit on everything.
*
* USE THIS INSTEAD OF SILENT FAILURES!
* Stop doing this: `const value = something || '';`
* Start doing this: `const value = something || silentDebug('something was undefined');`
* Start doing this: `const value = something || happy_path_error__with_fallback('something was undefined');`
*
* Logs to ~/.claude-mem/silent.log and returns a fallback value.
* Check logs with `npm run logs:silent`
*
* Usage:
* import { silentDebug } from '../utils/silent-debug.js';
* import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
*
* const title = obs.title || silentDebug('obs.title missing', { obs });
* const name = user.name || silentDebug('user.name missing', { user }, 'Anonymous');
* const title = obs.title || happy_path_error__with_fallback('obs.title missing', { obs });
* const name = user.name || happy_path_error__with_fallback('user.name missing', { user }, 'Anonymous');
*
* try {
* doSomething();
* } catch (error) {
* silentDebug('doSomething failed', { error });
* happy_path_error__with_fallback('doSomething failed', { error });
* }
*/
@@ -30,13 +32,13 @@ import { join } from 'path';
const LOG_FILE = join(homedir(), '.claude-mem', 'silent.log');
/**
* Write a debug message to silent.log and return fallback value
* @param message - The message to log
* Write an error message to silent.log and return fallback value
* @param message - Error message describing what went wrong
* @param data - Optional data to include (will be JSON stringified)
* @param fallback - Value to return (defaults to empty string)
* @returns The fallback value (for use in || fallbacks)
*/
export function silentDebug(message: string, data?: any, fallback: string = ''): string {
export function happy_path_error__with_fallback(message: string, data?: any, fallback: string = ''): string {
const timestamp = new Date().toISOString();
// Capture stack trace to get caller location
@@ -51,7 +53,7 @@ export function silentDebug(message: string, data?: any, fallback: string = ''):
? `${callerMatch[1].split('/').pop()}:${callerMatch[2]}`
: 'unknown';
let logLine = `[${timestamp}] [${location}] ${message}`;
let logLine = `[${timestamp}] [HAPPY-PATH-ERROR] [${location}] ${message}`;
if (data !== undefined) {
try {
@@ -84,3 +86,9 @@ export function clearSilentLog(): void {
// Ignore errors
}
}
/**
* @deprecated Use happy_path_error__with_fallback instead
* Backward compatibility alias for silentDebug
*/
export const silentDebug = happy_path_error__with_fallback;
+5 -5
View File
@@ -11,7 +11,7 @@
* This keeps the worker service simple and follows one-way data stream.
*/
import { silentDebug } from './silent-debug.js';
import { happy_path_error__with_fallback } from './silent-debug.js';
/**
* Maximum number of tags allowed in a single content block
@@ -41,14 +41,14 @@ function countTags(content: string): number {
*/
export function stripMemoryTagsFromJson(content: string): string {
if (typeof content !== 'string') {
silentDebug('[tag-stripping] received non-string for JSON context:', { type: typeof content });
happy_path_error__with_fallback('[tag-stripping] received non-string for JSON context:', { type: typeof content });
return '{}'; // Safe default for JSON context
}
// ReDoS protection: limit tag count before regex processing
const tagCount = countTags(content);
if (tagCount > MAX_TAG_COUNT) {
silentDebug('[tag-stripping] tag count exceeds limit, truncating:', {
happy_path_error__with_fallback('[tag-stripping] tag count exceeds limit, truncating:', {
tagCount,
maxAllowed: MAX_TAG_COUNT,
contentLength: content.length
@@ -73,14 +73,14 @@ export function stripMemoryTagsFromJson(content: string): string {
*/
export function stripMemoryTagsFromPrompt(content: string): string {
if (typeof content !== 'string') {
silentDebug('[tag-stripping] received non-string for prompt context:', { type: typeof content });
happy_path_error__with_fallback('[tag-stripping] received non-string for prompt context:', { type: typeof content });
return ''; // Safe default for prompt content
}
// ReDoS protection: limit tag count before regex processing
const tagCount = countTags(content);
if (tagCount > MAX_TAG_COUNT) {
silentDebug('[tag-stripping] tag count exceeds limit, truncating:', {
happy_path_error__with_fallback('[tag-stripping] tag count exceeds limit, truncating:', {
tagCount,
maxAllowed: MAX_TAG_COUNT,
contentLength: content.length