Compare commits

...

9 Commits

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 14:52:58 -05:00
Alex Newman ad8ac7970d fix: Chroma connection errors and remove dead last_user_message code (#525)
* fix: distinguish connection errors from collection-not-found in ChromaSync

Previously, ensureCollection() caught ALL errors from chroma_get_collection_info
and assumed they meant "collection doesn't exist". This caused connection errors
like "Not connected" to trigger unnecessary collection creation attempts.

Now connection-related errors are re-thrown immediately instead of being
misinterpreted as missing collections.

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

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

* fix: improve error handling for Chroma connection and collection creation

* fix: remove dead last_user_message from summarize flow

The last_user_message field was extracted from transcripts but never used.
In Claude Code transcripts, "user" type messages are mostly tool_results,
not actual user input. The user's original request is already stored in
user_prompts table.

This removes the false warning "Missing last_user_message when queueing
summary" which was complaining about missing data that didn't exist and
wasn't needed.

Changes:
- summary-hook: Only extract last_assistant_message
- SessionRoutes: Remove last_user_message from request body handling
- SessionManager.queueSummarize: Remove lastUserMessage parameter
- PendingMessage interface: Remove last_user_message field
- SDKSession interface: Remove last_user_message field
- All agents: Remove last_user_message from buildSummaryPrompt calls

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

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

* build artifacts for plugin

* Enhance error handling across multiple services

- Improved logging in `BranchManager.ts` to capture recovery checkout failures.
- Updated `PaginationHelper.ts` to log when file paths are plain strings instead of valid JSON.
- Enhanced error logging in `SDKAgent.ts` for Claude executable detection failures.
- Added logging for plain string handling in `SearchManager.ts` for files read and edited.
- Improved logging in `paths.ts` for git root detection failures.
- Enhanced JSON parsing error handling in `timeline-formatting.ts` with previews of failed inputs.
- Updated `transcript-parser.ts` to log summary of parse errors after processing transcript lines.
- Established a baseline for error handling practices in `error-handling-baseline.txt`.
- Documented error handling anti-pattern rules in `CLAUDE.md` to prevent silent failures and improve code quality.

* Add error handling anti-pattern detection script and guidelines

- Introduced `detect-error-handling-antipatterns.ts` to identify common error handling issues in TypeScript code.
- Created comprehensive documentation in `CLAUDE.md` outlining forbidden patterns, allowed patterns, and critical path protection rules.
- Implemented checks for empty catch blocks, logging practices, and try-catch block sizes to prevent silent failures and improve debugging.
- Established a reporting mechanism to summarize detected anti-patterns with severity levels.

* feat: add console filter bar and log line parsing with filtering capabilities

- Introduced a console filter bar with options to filter logs by level and component.
- Implemented parsing of log lines to extract structured data including timestamp, level, component, and correlation ID.
- Added functionality to toggle individual and all levels/components for filtering.
- Enhanced log line rendering with color coding based on log level and special message types.
- Improved responsiveness of the filter bar for smaller screens.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 14:45:50 -05:00
Alex Newman 65d1b52400 docs: update CHANGELOG.md for v8.5.3 2026-01-01 23:42:36 -05:00
Alex Newman b5e266fc80 chore: bump version to 8.5.3
Update version across all plugin manifests and build artifacts.
2026-01-01 23:41:20 -05:00
Alex Newman 417acb0f81 fix: comprehensive error handling improvements and architecture documentation (#522)
* Add enforceable anti-pattern detection for try-catch abuse

PROBLEM:
- Overly-broad try-catch blocks waste 10+ hours of debugging time
- Empty catch blocks silently swallow errors
- AI assistants use try-catch to paper over uncertainty instead of doing research

SOLUTION:
1. Created detect-error-handling-antipatterns.ts test
   - Detects empty catch blocks (45 CRITICAL found)
   - Detects catch without logging (45 CRITICAL total)
   - Detects large try blocks (>10 lines)
   - Detects generic catch without type checking
   - Detects catch-and-continue on critical paths
   - Exit code 1 if critical issues found

2. Updated CLAUDE.md with MANDATORY ERROR HANDLING RULES
   - 5-question pre-flight checklist before any try-catch
   - FORBIDDEN patterns with examples
   - ALLOWED patterns with examples
   - Meta-rule: UNCERTAINTY TRIGGERS RESEARCH, NOT TRY-CATCH
   - Critical path protection list

3. Created comprehensive try-catch audit report
   - Documents all 96 try-catch blocks in worker service
   - Identifies critical issue at worker-service.ts:748-750
   - Categorizes patterns and provides recommendations

This is enforceable via test, not just instructions that can be ignored.

Current state: 163 anti-patterns detected (45 critical, 47 high, 71 medium)
Next: Fix critical issues identified by test

🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add logging to 5 critical empty catch blocks (Wave 1)

Wave 1 of error handling cleanup - fixing empty catch blocks that
silently swallow errors without any trace.

Fixed files:
- src/bin/import-xml-observations.ts:80 - Log skipped invalid JSON
- src/utils/bun-path.ts:33 - Log when bun not in PATH
- src/utils/cursor-utils.ts:44 - Log failed registry reads
- src/utils/cursor-utils.ts:149 - Log corrupt MCP config
- src/shared/worker-utils.ts:128 - Log failed health checks

All catch blocks now have proper logging with context and error details.

Progress: 41 → 39 CRITICAL issues remaining

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

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

* fix: add logging to promise catches on critical paths (Wave 2)

Wave 2 of error handling cleanup - fixing empty promise catch handlers
that silently swallow errors on critical code paths. These are the
patterns that caused the 10-hour debugging session.

Fixed empty promise catches:
- worker-service.ts:642 - Background initialization failures
- SDKAgent.ts:372,446 - Session processor errors
- GeminiAgent.ts:408,475 - Finalization failures
- OpenRouterAgent.ts:451,518 - Finalization failures
- SessionManager.ts:289 - Generator promise failures

Added justification comments to catch-and-continue blocks:
- worker-service.ts:68 - PID file removal (cleanup, non-critical)
- worker-service.ts:130 - Cursor context update (non-critical)

All promise rejection handlers now log errors with context, preventing
silent failures that were nearly impossible to debug.

Note: The anti-pattern detector only tracks try-catch blocks, not
standalone promise chains. These fixes address the root cause of the
original 10-hour debugging session even though the detector count
remains unchanged.

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

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

* fix: add logging and documentation to error handling patterns (Wave 3)

Wave 3 of error handling cleanup - comprehensive review and fixes for
remaining critical issues identified by the anti-pattern detector.

Changes organized by severity:

**Wave 3.1: Fixed 2 EMPTY_CATCH blocks**
- worker-service.ts:162 - Health check polling now logs failures
- worker-service.ts:610 - Process cleanup logs failures

**Wave 3.2: Reviewed 12 CATCH_AND_CONTINUE patterns**
- Verified all are correct (log errors AND exit/return HTTP errors)
- Added justification comment to session recovery (line 829)
- All patterns properly notify callers of failures

**Wave 3.3: Fixed 29 NO_LOGGING_IN_CATCH issues**

Added logging to 16 catch blocks:
- UI layer: useSettings.ts, useContextPreview.ts (console logging)
- Servers: mcp-server.ts health checks and tool execution
- Worker: version fetch, cleanup, config corruption
- Routes: error handler, session recovery, settings validation
- Services: branch checkout, timeline queries

Documented 13 intentional exceptions with comments explaining why:
- Hot paths (port checks, process checks in tight loops)
- Error accumulation (transcript parser collects for batch retrieval)
- Special cases (logger can't log its own failures)
- Fallback parsing (JSON parse in optional data structures)

All changes follow error handling guidelines from CLAUDE.md:
- Appropriate log levels (error/warn/debug)
- Context objects with relevant details
- Descriptive messages explaining failures
- Error extraction pattern for Error instances

Progress: 41 → 29 detector warnings
Remaining warnings are conservative flags on verified-correct patterns
(catch-and-continue blocks that properly log + notify callers).

Build verified successful. All error handling now provides visibility
for debugging while avoiding excessive logging on hot paths.

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

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

* feat: add queue:clear command to remove failed messages

Added functionality to clear failed messages from the observation queue:

**Changes:**
- PendingMessageStore: Added clearFailed() method to delete failed messages
- DataRoutes: Added DELETE /api/pending-queue/failed endpoint
- CLI: Created scripts/clear-failed-queue.ts for interactive queue clearing
- package.json: Added npm run queue:clear script

**Usage:**
  npm run queue:clear          # Interactive - prompts for confirmation
  npm run queue:clear -- --force  # Non-interactive - clears without prompt

Failed messages are observations that exceeded max retry count. They
remain in the queue for debugging but won't be processed. This command
removes them to clean up the queue.

Works alongside existing queue:check and queue:process commands to
provide complete queue management capabilities.

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

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

* feat: add --all flag to queue:clear for complete queue reset

Extended queue clearing functionality to support clearing all messages,
not just failed ones.

**Changes:**
- PendingMessageStore: Added clearAll() method to clear pending, processing, and failed
- DataRoutes: Added DELETE /api/pending-queue/all endpoint
- clear-failed-queue.ts: Added --all flag to clear everything
- Updated help text and UI to distinguish between failed-only and all-clear modes

**Usage:**
  npm run queue:clear              # Clear failed only (interactive)
  npm run queue:clear -- --all     # Clear ALL messages (interactive)
  npm run queue:clear -- --all --force  # Clear all without confirmation

The --all flag provides a complete queue reset, removing pending,
processing, and failed messages. Useful when you want a fresh start
or need to cancel stuck sessions.

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

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

* feat: add comprehensive documentation for session ID architecture and validation tests

* feat: add logs viewer with clear functionality to UI

- Add LogsRoutes API endpoint for fetching and clearing worker logs
- Create LogsModal component with auto-refresh and clear button
- Integrate logs viewer button into Header component
- Add comprehensive CSS styling for logs modal
- Logs accessible via new document icon button in header

Logs viewer features:
- Display last 1000 lines of current day's log file
- Auto-refresh toggle (2s interval)
- Clear logs button with confirmation
- Monospace font for readable log output
- Responsive modal design matching existing UI

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

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

* refactor: redesign logs as Chrome DevTools-style console drawer

Major UX improvements to match Chrome DevTools console:
- Convert from modal to bottom drawer that slides up
- Move toggle button to bottom-left corner (floating button)
- Add draggable resize handle for height adjustment
- Use plain monospace font (SF Mono/Monaco/Consolas) instead of Monaspace
- Simplify controls with icon-only buttons
- Add Console tab UI matching DevTools aesthetic

Changes:
- Renamed LogsModal to LogsDrawer with drawer implementation
- Added resize functionality with mouse drag
- Removed logs button from header
- Added floating console toggle button in bottom-left
- Updated all CSS to match Chrome console styling
- Minimum height: 150px, maximum: window height - 100px

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

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

* fix: suppress /api/logs endpoint logging to reduce noise

Skip logging GET /api/logs requests in HTTP middleware to prevent
log spam from auto-refresh polling (every 2s). Keeps the auto-refresh
feature functional while eliminating the repetitive log entries.

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

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

* refactor: enhance error handling guidelines with approved overrides for justified exceptions

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 23:38:22 -05:00
Alex Newman c2fbb39fd0 Refactor error handling in WorkerService and SDKAgent
- Removed unnecessary logging for aborted sessions in WorkerService.
- Simplified the finally block in WorkerService by removing crash recovery logging.
- Cleaned up the try-catch structure in SDKAgent's startSession method, removing redundant error handling.
2026-01-01 20:46:39 -05:00
Alex Newman 564249f533 Implement code changes to enhance functionality and improve performance 2026-01-01 20:30:07 -05:00
Alex Newman f5b0fbc7cb Refactor code structure for improved readability and maintainability 2026-01-01 20:28:48 -05:00
Alex Newman ff86e21697 docs: update CHANGELOG.md for v8.5.2 release 2025-12-31 16:52:59 -05:00
56 changed files with 4480 additions and 757 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "8.5.2",
"version": "8.5.4",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+189 -285
View File
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -1,9 +1,5 @@
/* To @claude: be vigilant about only leaving evergreen context in this file, claude-mem handles working context separately. */
# Claude-Mem: AI Development Instructions
## What This Project Is
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
## Architecture
@@ -14,7 +10,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
**Worker Service** (`src/services/worker-service.ts`) - Express API on port 37777, Bun-managed, handles AI processing asynchronously
**Database** (`src/services/sqlite/`) - SQLite3 at `~/.claude-mem/claude-mem.db`
**Database** (`src/services/sqlite/`) - SQLite3 at `~/.claude-mem/claude-mem.db`
**Search Skill** (`plugin/skills/mem-search/SKILL.md`) - HTTP API for searching past work, auto-invoked when users ask about history
@@ -80,6 +76,6 @@ Claude-mem is designed with a clean separation between open-source core function
This architecture preserves the open-source nature of the project while enabling sustainable development through optional paid features.
# Important
## Important
No need to edit the changelog ever, it's generated automatically.
+243
View File
@@ -0,0 +1,243 @@
# Session ID Architecture
## Overview
Claude-mem uses **two distinct session IDs** to track conversations and memory:
1. **`contentSessionId`** - The user's Claude Code conversation session ID
2. **`memorySessionId`** - The SDK agent's internal session ID for resume functionality
## Critical Architecture
### Initialization Flow
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Hook creates session │
│ createSDKSession(contentSessionId, project, prompt) │
│ │
│ Database state: │
│ ├─ content_session_id: "user-session-123" │
│ └─ memory_session_id: "user-session-123" (placeholder) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. SDKAgent starts, checks hasRealMemorySessionId │
│ const hasReal = memorySessionId !== contentSessionId │
│ → FALSE (they're equal) │
│ → Resume NOT used (fresh SDK session) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. First SDK message arrives with session_id │
│ updateMemorySessionId(sessionDbId, "sdk-gen-abc123") │
│ │
│ Database state: │
│ ├─ content_session_id: "user-session-123" │
│ └─ memory_session_id: "sdk-gen-abc123" (real!) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. Subsequent prompts use resume │
│ const hasReal = memorySessionId !== contentSessionId │
│ → TRUE (they're different) │
│ → Resume parameter: { resume: "sdk-gen-abc123" } │
└─────────────────────────────────────────────────────────────┘
```
### Observation Storage
**CRITICAL**: Observations are stored with `contentSessionId`, NOT the captured SDK `memorySessionId`.
```typescript
// SDKAgent.ts line 332-333
this.dbManager.getSessionStore().storeObservation(
session.contentSessionId, // ← contentSessionId, not memorySessionId!
session.project,
obs,
// ...
);
```
Even though the parameter is named `memorySessionId`, it receives `contentSessionId`. This means:
- Database column: `observations.memory_session_id`
- Stored value: `contentSessionId` (the user's session ID)
- Foreign key: References `sdk_sessions.memory_session_id`
The observations are linked to the session via the initial placeholder value that never changes from the observation's perspective.
## Key Invariants
### 1. Placeholder Detection
```typescript
const hasRealMemorySessionId =
session.memorySessionId &&
session.memorySessionId !== session.contentSessionId;
```
- When `memorySessionId === contentSessionId` → Placeholder state
- When `memorySessionId !== contentSessionId` → Real SDK session captured
### 2. Resume Safety
**NEVER** use `contentSessionId` for resume:
```typescript
// ❌ FORBIDDEN - Would resume user's session instead of memory session!
query({ resume: contentSessionId })
// ✅ CORRECT - Only resume when we have real memory session ID
query({
...(hasRealMemorySessionId && { resume: memorySessionId })
})
```
### 3. Session Isolation
- Each `contentSessionId` maps to exactly one database session
- Each database session has one `memorySessionId` (initially placeholder, then captured)
- Observations from different content sessions must NEVER mix
### 4. Foreign Key Integrity
- Observations reference `sdk_sessions.memory_session_id`
- Initially, both `sdk_sessions.memory_session_id` and `observations.memory_session_id` contain `contentSessionId`
- When SDK session ID is captured, `sdk_sessions.memory_session_id` updates but observations stay with `contentSessionId`
- Observations remain retrievable via `contentSessionId`
## Testing Strategy
The test suite validates all critical invariants:
### Test File
`tests/session_id_usage_validation.test.ts`
### Test Categories
1. **Placeholder Detection** - Validates `hasRealMemorySessionId` logic
2. **Observation Storage** - Confirms observations use `contentSessionId`
3. **Resume Safety** - Prevents `contentSessionId` from being used for resume
4. **Cross-Contamination Prevention** - Ensures session isolation
5. **Foreign Key Integrity** - Validates cascade behavior
6. **Session Lifecycle** - Tests create → capture → resume flow
7. **Edge Cases** - Handles NULL, duplicate IDs, etc.
### Running Tests
```bash
# Run all session ID tests
bun test tests/session_id_usage_validation.test.ts
# Run all tests
bun test
# Run with verbose output
bun test --verbose
```
## Common Pitfalls
### ❌ Using memorySessionId for observations
```typescript
// WRONG - Don't use the captured SDK session ID
storeObservation(session.memorySessionId, ...)
```
### ❌ Resuming with placeholder value
```typescript
// WRONG - Would resume user's session!
if (session.memorySessionId) {
query({ resume: session.memorySessionId })
}
```
### ❌ Assuming memorySessionId is always set
```typescript
// WRONG - Can be NULL or equal to contentSessionId
const resumeId = session.memorySessionId
```
## Correct Usage Patterns
### ✅ Storing observations
```typescript
// Always use contentSessionId
storeObservation(session.contentSessionId, project, obs, ...)
```
### ✅ Checking for real memory session ID
```typescript
const hasRealMemorySessionId =
session.memorySessionId &&
session.memorySessionId !== session.contentSessionId;
```
### ✅ Using resume parameter
```typescript
query({
prompt: messageGenerator,
options: {
...(hasRealMemorySessionId && { resume: session.memorySessionId }),
// ... other options
}
})
```
## Debugging Tips
### Check session state
```sql
-- See both session IDs
SELECT
id,
content_session_id,
memory_session_id,
CASE
WHEN memory_session_id = content_session_id THEN 'PLACEHOLDER'
ELSE 'CAPTURED'
END as state
FROM sdk_sessions
WHERE content_session_id = 'your-session-id';
```
### Find orphaned observations
```sql
-- Should return 0 rows if FK integrity is maintained
SELECT o.*
FROM observations o
LEFT JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
WHERE s.id IS NULL;
```
### Verify observation linkage
```sql
-- See which observations belong to a session
SELECT
o.id,
o.title,
o.memory_session_id,
s.content_session_id,
s.memory_session_id as session_memory_id
FROM observations o
JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
WHERE s.content_session_id = 'your-session-id';
```
## References
- **Implementation**: `src/services/worker/SDKAgent.ts` (lines 72-94)
- **Database Schema**: `src/services/sqlite/SessionStore.ts` (line 95-104)
- **Tests**: `tests/session_id_usage_validation.test.ts`
- **Related Tests**: `tests/session_id_refactor.test.ts`
+688
View File
@@ -0,0 +1,688 @@
🔍 Scanning for error handling anti-patterns...
Found 80 TypeScript files
═══════════════════════════════════════════════════════════════
ERROR HANDLING ANTI-PATTERNS DETECTED
═══════════════════════════════════════════════════════════════
Found 153 anti-patterns:
🔴 CRITICAL: 26
🟠 HIGH: 47
🟡 MEDIUM: 80
🔴 CRITICAL ISSUES (Fix immediately - these cause silent failures):
─────────────────────────────────────────────────────────────
📁 src/utils/transcript-parser.ts:44
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Parse errors accumulated in parseErrors array for batch access, logging each line would be excessive
this.parseErrors.push({
lineNumber: index + 1,
error: error instanceof Error ? error.message : String(error),
... (2 more lines)
📁 src/shared/timeline-formatting.ts:18
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (err) {
// [POSSIBLY RELEVANT]: Expected JSON parse failures for malformed data fields, too frequent to log
return [];
}
📁 src/shared/paths.ts:105
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected when not in git repo or git unavailable, common fallback path
return basename(process.cwd());
}
📁 src/sdk/prompts.ts:98
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected JSON parse failures for plain string tool inputs, normal fallback
toolInput = obs.tool_input;
}
📁 src/sdk/prompts.ts:105
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected JSON parse failures for plain string tool outputs, normal fallback
toolOutput = obs.tool_output;
}
📁 src/services/worker-service.ts:68
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: PID file cleanup is non-critical, log full error and continue shutdown
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE }, error as Error);
}
📁 src/services/worker-service.ts:131
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Context update is non-critical, log full error and continue
logger.warn('CURSOR', 'Failed to update context file', { projectName }, error as Error);
}
📁 src/services/worker-service.ts:152
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected failure when port is free, called frequently for health checks
return false;
}
📁 src/services/worker-service.ts:165
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected failures during startup health check, will retry
logger.debug('SYSTEM', 'Service not ready yet, will retry', { port }, error as Error);
}
📁 src/services/worker-service.ts:368
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Shutdown must complete, log error and exit with failure code
logger.error('SYSTEM', 'Error during shutdown', {}, error as Error);
process.exit(1);
}
📁 src/services/worker-service.ts:623
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Process may have already exited during cleanup, expected failure
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', { pid }, error as Error);
}
📁 src/services/worker-service.ts:632
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Process may have already exited during cleanup, expected failure
logger.debug('SYSTEM', 'Process already exited', { pid }, error as Error);
}
📁 src/services/worker-service.ts:838
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// Recovery is best-effort - skip failed sessions and continue with others
logger.warn('SYSTEM', `Failed to process session ${sessionDbId}`, {}, error as Error);
result.sessionsSkipped++;
}
📁 src/services/worker-service.ts:987
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch {
// Process may have already exited - continue shutdown
logger.debug('SYSTEM', 'Process already exited during force kill', { pid });
}
📁 src/services/worker-service.ts:1004
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// Expected: process has exited
// Not logging - this is called in a tight loop during cleanup
return false;
}
📁 src/services/worker-service.ts:1095
❌ EMPTY_CATCH
Empty catch block - errors are silently swallowed. User will waste hours debugging.
Code:
} catch {
// Start fresh if corrupt
}
📁 src/services/worker-service.ts:1301
❌ EMPTY_CATCH
Empty catch block - errors are silently swallowed. User will waste hours debugging.
Code:
} catch {
// CLI not found
}
📁 src/services/worker-service.ts:1413
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// Start fresh if corrupt
logger.warn('SYSTEM', 'Corrupt mcp.json, creating new config', { path: mcpJsonPath, error: error instanceof Error ? error.message : String(error) });
config = { mcpServers: {} };
}
📁 src/services/worker-service.ts:1670
❌ EMPTY_CATCH
Empty catch block - errors are silently swallowed. User will waste hours debugging.
Code:
} catch {
// Worker not running - that's ok, context will be generated after first session
}
📁 src/services/worker-service.ts:2050
❌ PROMISE_CATCH_NO_LOGGING
Promise .catch() without logging - errors are silently swallowed.
Code:
.catch((error) => {
logger.failure('SYSTEM', 'Worker failed to start', {}, error as Error);
removePidFile();
process.exit(1);
});
📁 src/services/worker/SDKAgent.ts:545
❌ CATCH_AND_CONTINUE_CRITICAL_PATH
Critical path continues after error - may cause silent data corruption.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected failure when claude not in PATH, falls through to clear error message below
logger.debug('SDK', 'Claude executable auto-detection failed', {}, error as Error);
}
📁 src/services/worker/SearchManager.ts:1403
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected JSON parse failures for plain string file lists, normal fallback
if (summary.files_read.trim()) {
lines.push(`**Files Read:** ${summary.files_read}`);
}
... (1 more lines)
📁 src/services/worker/SearchManager.ts:1418
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error) {
// [POSSIBLY RELEVANT]: Expected JSON parse failures for plain string file lists, normal fallback
if (summary.files_edited.trim()) {
lines.push(`**Files Edited:** ${summary.files_edited}`);
}
... (1 more lines)
📁 src/services/worker/PaginationHelper.ts:54
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (err) {
// [POSSIBLY RELEVANT]: Expected JSON parse failures for plain string file paths, normal fallback
return filePathsStr;
}
📁 src/services/context-generator.ts:202
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (parseError) {
// [POSSIBLY RELEVANT]: Expected malformed JSON lines in transcript, logging each would be excessive
continue;
}
📁 src/services/context-generator.ts:226
❌ NO_LOGGING_IN_CATCH
Catch block has no logging - errors occur invisibly.
Code:
} catch (error: any) {
if (error.code === 'ERR_DLOPEN_FAILED') {
unlinkSync(VERSION_MARKER_PATH);
} catch (unlinkError) {
// [POSSIBLY RELEVANT]: Marker file may not exist during first run, expected cleanup failure
... (1 more lines)
🟠 HIGH PRIORITY:
─────────────────────────────────────────────────────────────
📁 src/ui/viewer/hooks/useSSE.ts:50 - LARGE_TRY_BLOCK
Try block has 33 lines - too broad. Multiple errors lumped together.
📁 src/ui/viewer/hooks/usePagination.ts:54 - LARGE_TRY_BLOCK
Try block has 19 lines - too broad. Multiple errors lumped together.
📁 src/ui/viewer/hooks/useSettings.ts:64 - LARGE_TRY_BLOCK
Try block has 14 lines - too broad. Multiple errors lumped together.
📁 src/ui/viewer/hooks/useContextPreview.ts:47 - LARGE_TRY_BLOCK
Try block has 11 lines - too broad. Multiple errors lumped together.
📁 src/bin/import-xml-observations.ts:62 - LARGE_TRY_BLOCK
Try block has 12 lines - too broad. Multiple errors lumped together.
📁 src/bin/import-xml-observations.ts:134 - LARGE_TRY_BLOCK
Try block has 15 lines - too broad. Multiple errors lumped together.
📁 src/bin/import-xml-observations.ts:167 - LARGE_TRY_BLOCK
Try block has 13 lines - too broad. Multiple errors lumped together.
📁 src/servers/mcp-server.ts:52 - LARGE_TRY_BLOCK
Try block has 14 lines - too broad. Multiple errors lumped together.
📁 src/servers/mcp-server.ts:97 - LARGE_TRY_BLOCK
Try block has 21 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:55 - LARGE_TRY_BLOCK
Try block has 67 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:227 - LARGE_TRY_BLOCK
Try block has 38 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:346 - LARGE_TRY_BLOCK
Try block has 40 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:427 - LARGE_TRY_BLOCK
Try block has 43 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:495 - LARGE_TRY_BLOCK
Try block has 15 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:532 - LARGE_TRY_BLOCK
Try block has 35 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:599 - LARGE_TRY_BLOCK
Try block has 13 lines - too broad. Multiple errors lumped together.
📁 src/services/sqlite/SessionStore.ts:1550 - LARGE_TRY_BLOCK
Try block has 27 lines - too broad. Multiple errors lumped together.
📁 src/services/worker-service.ts:438 - LARGE_TRY_BLOCK
Try block has 16 lines - too broad. Multiple errors lumped together.
📁 src/services/worker-service.ts:526 - LARGE_TRY_BLOCK
Try block has 11 lines - too broad. Multiple errors lumped together.
📁 src/services/worker-service.ts:666 - LARGE_TRY_BLOCK
Try block has 56 lines - too broad. Multiple errors lumped together.
📁 src/services/worker-service.ts:814 - LARGE_TRY_BLOCK
Try block has 15 lines - too broad. Multiple errors lumped together.
📁 src/services/worker-service.ts:1638 - LARGE_TRY_BLOCK
Try block has 24 lines - too broad. Multiple errors lumped together.
📁 src/services/worker-service.ts:1753 - LARGE_TRY_BLOCK
Try block has 28 lines - too broad. Multiple errors lumped together.
📁 src/services/sync/ChromaSync.ts:99 - LARGE_TRY_BLOCK
Try block has 28 lines - too broad. Multiple errors lumped together.
📁 src/services/sync/ChromaSync.ts:344 - LARGE_TRY_BLOCK
Try block has 14 lines - too broad. Multiple errors lumped together.
📁 src/services/sync/ChromaSync.ts:534 - LARGE_TRY_BLOCK
Try block has 32 lines - too broad. Multiple errors lumped together.
📁 src/services/sync/ChromaSync.ts:609 - LARGE_TRY_BLOCK
Try block has 106 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/GeminiAgent.ts:144 - LARGE_TRY_BLOCK
Try block has 76 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/BranchManager.ts:120 - LARGE_TRY_BLOCK
Try block has 13 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/BranchManager.ts:268 - LARGE_TRY_BLOCK
Try block has 21 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:120 - LARGE_TRY_BLOCK
Try block has 43 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:382 - LARGE_TRY_BLOCK
Try block has 13 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:642 - LARGE_TRY_BLOCK
Try block has 22 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:726 - LARGE_TRY_BLOCK
Try block has 18 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:818 - LARGE_TRY_BLOCK
Try block has 14 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:888 - LARGE_TRY_BLOCK
Try block has 16 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:958 - LARGE_TRY_BLOCK
Try block has 16 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:1028 - LARGE_TRY_BLOCK
Try block has 16 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:1098 - LARGE_TRY_BLOCK
Try block has 16 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:1181 - LARGE_TRY_BLOCK
Try block has 17 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:1282 - LARGE_TRY_BLOCK
Try block has 16 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:1493 - LARGE_TRY_BLOCK
Try block has 147 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/SearchManager.ts:1725 - LARGE_TRY_BLOCK
Try block has 15 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/OpenRouterAgent.ts:104 - LARGE_TRY_BLOCK
Try block has 77 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/http/routes/SessionRoutes.ts:151 - LARGE_TRY_BLOCK
Try block has 13 lines - too broad. Multiple errors lumped together.
📁 src/services/worker/http/routes/SessionRoutes.ts:185 - LARGE_TRY_BLOCK
Try block has 20 lines - too broad. Multiple errors lumped together.
📁 src/services/context-generator.ts:182 - LARGE_TRY_BLOCK
Try block has 15 lines - too broad. Multiple errors lumped together.
🟡 MEDIUM PRIORITY:
─────────────────────────────────────────────────────────────
📁 src/ui/viewer/hooks/useStats.ts:13 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/ui/viewer/hooks/useSSE.ts:93 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/ui/viewer/hooks/useTheme.ts:19 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/ui/viewer/hooks/useTheme.ts:64 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/ui/viewer/hooks/usePagination.ts:84 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/ui/viewer/hooks/useContextPreview.ts:31 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/ui/viewer/hooks/useContextPreview.ts:60 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/bin/import-xml-observations.ts:152 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/bin/import-xml-observations.ts:183 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/bin/import-xml-observations.ts:329 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/bin/import-xml-observations.ts:361 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/utils/logger.ts:55 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/utils/logger.ts:74 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/utils/logger.ts:269 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/shared/timeline-formatting.ts:18 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/shared/SettingsDefaultsManager.ts:152 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/shared/paths.ts:105 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/sdk/prompts.ts:98 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/sdk/prompts.ts:105 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/servers/mcp-server.ts:76 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/servers/mcp-server.ts:123 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/servers/mcp-server.ts:269 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:138 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:278 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:399 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:482 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:520 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:575 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:619 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:1489 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:1521 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sqlite/SessionStore.ts:1577 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:59 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:68 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:131 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:152 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:165 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:185 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:368 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:623 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:632 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:743 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:838 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:963 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:1004 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker-service.ts:1797 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/queue/SessionQueueProcessor.ts:31 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sync/ChromaSync.ts:578 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/sync/ChromaSync.ts:808 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SettingsManager.ts:45 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/BranchManager.ts:138 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/BranchManager.ts:243 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/BranchManager.ts:300 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SDKAgent.ts:545 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:185 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:398 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:676 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:754 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:838 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:912 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:982 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1052 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1127 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1214 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1311 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1403 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1418 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1700 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SearchManager.ts:1745 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/PaginationHelper.ts:54 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/http/BaseRouteHandler.ts:28 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/http/routes/SettingsRoutes.ts:76 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/http/routes/SessionRoutes.ts:165 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SessionManager.ts:208 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/worker/SessionManager.ts:256 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/domain/ModeManager.ts:146 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/domain/ModeManager.ts:163 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/domain/ModeManager.ts:173 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/context-generator.ts:202 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
📁 src/services/context-generator.ts:226 - GENERIC_CATCH
Catch block handles all errors identically - no error type discrimination.
═══════════════════════════════════════════════════════════════
REMINDER: Every try-catch must answer these questions:
1. What SPECIFIC error am I catching? (Name it)
2. Show me documentation proving this error can occur
3. Why can't this error be prevented?
4. What will the catch block DO? (Log + rethrow? Fallback?)
5. Why shouldn't this error propagate to the caller?
To approve an anti-pattern, add: // [APPROVED OVERRIDE]: reason
═══════════════════════════════════════════════════════════════
❌ FAILED: 26 critical error handling anti-patterns must be fixed.
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "8.5.2",
"version": "8.5.4",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
@@ -47,6 +47,7 @@
"worker:status": "bun plugin/scripts/worker-service.cjs status",
"queue:check": "bun scripts/check-pending-queue.ts",
"queue:process": "bun scripts/check-pending-queue.ts --process",
"queue:clear": "bun scripts/clear-failed-queue.ts",
"translate-readme": "bun scripts/translate-readme/cli.ts -v -o docs/i18n README.md",
"translate:tier1": "npm run translate-readme -- zh ja pt-br ko es de fr",
"translate:tier2": "npm run translate-readme -- he ar ru pl cs nl tr uk",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "8.5.2",
"version": "8.5.4",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem-plugin",
"version": "8.5.1",
"version": "8.5.3",
"private": true,
"description": "Runtime dependencies for claude-mem bundled hooks",
"type": "module",
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bun
import{stdin as L}from"process";import A from"path";import{homedir as G}from"os";import{readFileSync as K}from"fs";import{readFileSync as k,writeFileSync as v,existsSync as w}from"fs";import{join as F}from"path";import{homedir as W}from"os";var U="bugfix,feature,refactor,discovery,decision,change",R="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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){try{if(!w(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as x,existsSync as b,mkdirSync as H}from"fs";import{join as O}from"path";var S=(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))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");b(r)||H(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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}
import{stdin as L}from"process";import A from"path";import{homedir as G}from"os";import{readFileSync as K}from"fs";import{readFileSync as $,writeFileSync as v,existsSync as w}from"fs";import{join as F}from"path";import{homedir as W}from"os";var R="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 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){try{if(!w(t))return this.getAllDefaults();let r=$(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as x,existsSync as b,mkdirSync as H}from"fs";import{join as O}from"path";var S=(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))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");b(r)||H(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),u=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${u}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=S[t].padEnd(5),u=r.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let T="";if(n){let{sessionId:m,memorySessionId:q,correlationId:Q,...D}=n;Object.keys(D).length>0&&(T=` {${Object.entries(D).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${i}] [${E}] [${u}] ${c}${e}${T}${l}`;if(this.logFilePath)try{x(this.logFilePath,C+`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let T="";if(n){let{sessionId:m,memorySessionId:q,correlationId:Q,...D}=n;Object.keys(D).length>0&&(T=` {${Object.entries(D).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let C=`[${i}] [${E}] [${u}] ${c}${e}${T}${l}`;if(this.logFilePath)try{x(this.logFilePath,C+`
`,"utf8")}catch(m){process.stderr.write(`[LOGGER] Failed to write to log file: ${m}
`)}else process.stderr.write(C+`
`)}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`})}happyPathError(t,r,e,n,o=""){let c=((new Error().stack||"").split(`
@@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
${E}`),E}var X=A.join(G(),".claude","plugins","marketplaces","thedotmack"),At=d(p.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function j(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function V(){let s=A.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function B(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Y(){let s=V(),t=await B();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await j()){await Y();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}import J from"path";function N(s){if(!s||s.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=J.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function y(s){await I();let t=s?.cwd??process.cwd(),r=N(t),n=`http://127.0.0.1:${g()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await fetch(n);if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var z=process.argv.includes("--colors");if(L.isTTY||z)y(void 0).then(s=>{console.log(s),process.exit(0)});else{let s="";L.on("data",t=>s+=t),L.on("end",async()=>{let t;try{t=s.trim()?JSON.parse(s):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await y(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
${E}`),E}var X=A.join(G(),".claude","plugins","marketplaces","thedotmack"),At=d(p.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function j(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function V(){let s=A.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function Y(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function B(){let s=V(),t=await Y();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await j()){await B();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}import J from"path";function N(s){if(!s||s.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=J.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function y(s){await I();let t=s?.cwd??process.cwd(),r=N(t),n=`http://127.0.0.1:${g()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await fetch(n);if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var z=process.argv.includes("--colors");if(L.isTTY||z)y(void 0).then(s=>{console.log(s),process.exit(0)});else{let s="";L.on("data",t=>s+=t),L.on("end",async()=>{let t;try{t=s.trim()?JSON.parse(s):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await y(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bun
import{stdin as P}from"process";var T=JSON.stringify({continue:!0,suppressOutput:!0});import L from"path";import{homedir as G}from"os";import{readFileSync as X}from"fs";import{readFileSync as v,writeFileSync as w,existsSync as b}from"fs";import{join as F}from"path";import{homedir as H}from"os";var R="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 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){try{if(!b(t))return this.getAllDefaults();let r=v(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"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(_){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},_)}}let o={...this.DEFAULTS};for(let _ of Object.keys(this.DEFAULTS))n[_]!==void 0&&(o[_]=n[_]);return o}catch(r){return E.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as K,mkdirSync as x}from"fs";import{join as S}from"path";var f=(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))(f||{}),M=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=S(t,"logs");K(r)||x(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=S(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=S(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=f[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),_=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${_}:${s}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let _=this.formatTimestamp(new Date),s=f[t].padEnd(5),l=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
import{stdin as k}from"process";var S=JSON.stringify({continue:!0,suppressOutput:!0});import L from"path";import{homedir as G}from"os";import{readFileSync as X}from"fs";import{readFileSync as v,writeFileSync as w,existsSync as b}from"fs";import{join as F}from"path";import{homedir as H}from"os";var R="bugfix,feature,refactor,discovery,decision,change",h="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!b(t))return this.getAllDefaults();let r=v(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"),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 o={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(o[a]=n[a]);return o}catch(r){return E.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as x,mkdirSync as K}from"fs";import{join as T}from"path";var f=(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))(f||{}),M=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");x(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=f[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${s}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let a=this.formatTimestamp(new Date),s=f[t].padEnd(5),l=r.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let p="";if(n){let{sessionId:D,memorySessionId:Q,correlationId:Z,...d}=n;Object.keys(d).length>0&&(p=` {${Object.entries(d).map(([k,$])=>`${k}=${$}`).join(", ")}}`)}let m=`[${_}] [${s}] [${l}] ${a}${e}${p}${c}`;if(this.logFilePath)try{W(this.logFilePath,m+`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let p="";if(n){let{sessionId:D,memorySessionId:Q,correlationId:Z,...d}=n;Object.keys(d).length>0&&(p=` {${Object.entries(d).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${a}] [${s}] [${l}] ${_}${e}${p}${c}`;if(this.logFilePath)try{W(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(m+`
`)}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`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",p={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,p,n),o}},E=new M;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function N(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",_=t?` (port ${t})`:"",s=`${o}${_}
`)}else process.stderr.write(C+`
`)}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`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",p={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,p,n),o}},E=new M;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function U(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function N(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",s=`${o}${a}
`;return s+=`To restart the worker:
`,s+=`1. Exit Claude Code completely
@@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n}
${s}`),s}var j=L.join(G(),".claude","plugins","marketplaces","thedotmack"),Ct=h(A.HEALTH_CHECK),O=null;function u(){if(O!==null)return O;let i=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(i);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function V(){let i=u();return(await fetch(`http://127.0.0.1:${i}/api/readiness`)).ok}function B(){let i=L.join(j,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=u(),t=await fetch(`http://127.0.0.1:${i}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&E.debug("SYSTEM","Version check",{pluginVersion:i,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await V()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import z from"path";function y(i){if(!i||i.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=z.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function q(i){if(await I(),!i)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=i,n=y(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=u();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let _=await fetch(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!_.ok)throw new Error(`Session initialization failed: ${_.status}`);let s=await _.json(),l=s.sessionDbId,a=s.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:a,skipped:s.skipped}),s.skipped&&s.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${a} (fully private - skipped)`),console.log(T);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${a}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:a,userPrompt_length:c?.length});let p=await fetch(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:a})});if(!p.ok)throw new Error(`SDK agent start failed: ${p.status}`);console.log(T)}var C="";P.on("data",i=>C+=i);P.on("end",async()=>{let i;try{i=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await q(i)});
${s}`),s}var j=L.join(G(),".claude","plugins","marketplaces","thedotmack"),mt=U(A.HEALTH_CHECK),O=null;function u(){if(O!==null)return O;let i=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(i);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function V(){let i=u();return(await fetch(`http://127.0.0.1:${i}/api/readiness`)).ok}function B(){let i=L.join(j,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=u(),t=await fetch(`http://127.0.0.1:${i}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&E.debug("SYSTEM","Version check",{pluginVersion:i,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await V()){await J();return}}catch(e){E.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import z from"path";function y(i){if(!i||i.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=z.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function q(i){if(await I(),!i)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=i,n=y(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=u();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let a=await fetch(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let s=await a.json(),l=s.sessionDbId,_=s.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:s.skipped}),s.skipped&&s.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(S);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let p=await fetch(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_})});if(!p.ok)throw new Error(`SDK agent start failed: ${p.status}`);console.log(S)}var m="";k.on("data",i=>m+=i);k.on("end",async()=>{let i;try{i=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await q(i)});
+5 -5
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bun
import{stdin as y}from"process";var U=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as k,writeFileSync as v,existsSync as w}from"fs";import{join as F}from"path";import{homedir as H}from"os";var R="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!w(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as b,mkdirSync as G}from"fs";import{join as M}from"path";var S=(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))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=M(t,"logs");b(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=M(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=M(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=S[t].padEnd(5),l=r.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let g="";o!=null&&(o instanceof Error?g=this.getLevel()===0?`
import{stdin as y}from"process";var U=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as k,writeFileSync as v,existsSync as w}from"fs";import{join as F}from"path";import{homedir as H}from"os";var R="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!w(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as b,mkdirSync as x}from"fs";import{join as S}from"path";var M=(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))(M||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=S(t,"logs");b(r)||x(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=S(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=S(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=M[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=M[t].padEnd(5),l=r.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let g="";o!=null&&(o instanceof Error?g=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?g=`
`+JSON.stringify(o,null,2):g=" "+this.formatData(o));let u="";if(n){let{sessionId:D,memorySessionId:q,correlationId:z,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${i}] [${E}] [${l}] ${c}${e}${u}${g}`;if(this.logFilePath)try{W(this.logFilePath,C+`
`+JSON.stringify(o,null,2):g=" "+this.formatData(o));let O="";if(n){let{sessionId:D,memorySessionId:q,correlationId:z,...m}=n;Object.keys(m).length>0&&(O=` {${Object.entries(m).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${i}] [${E}] [${l}] ${c}${e}${O}${g}`;if(this.logFilePath)try{W(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+`
`)}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`})}happyPathError(t,r,e,n,o=""){let c=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=c?`${c[1].split("/").pop()}:${c[2]}`:"unknown",u={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},_=new f;import A from"path";import{homedir as x}from"os";import{readFileSync as K}from"fs";var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function h(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${o}${i}
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=c?`${c[1].split("/").pop()}:${c[2]}`:"unknown",O={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},_=new f;import A from"path";import{homedir as G}from"os";import{readFileSync as K}from"fs";var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${o}${i}
`;return E+=`To restart the worker:
`,E+=`1. Exit Claude Code completely
@@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?g=`
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
${E}`),E}var X=A.join(x(),".claude","plugins","marketplaces","thedotmack"),At=I(p.HEALTH_CHECK),T=null;function O(){if(T!==null)return T;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function V(){let s=O();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=A.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function B(){let s=O(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Y(){let s=j(),t=await B();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await Y();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:O(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function J(s){if(await N(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=O(),E=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let l=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!l.ok)throw new Error(`Observation storage failed: ${l.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var L="";y.on("data",s=>L+=s);y.on("end",async()=>{let s;try{s=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await J(s)});
${E}`),E}var X=A.join(G(),".claude","plugins","marketplaces","thedotmack"),At=h(p.HEALTH_CHECK),T=null;function u(){if(T!==null)return T;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function V(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=A.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function B(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Y(){let s=j(),t=await B();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await Y();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function J(s){if(await N(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=u(),E=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let l=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!l.ok)throw new Error(`Observation storage failed: ${l.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var L="";y.on("data",s=>L+=s);y.on("end",async()=>{let s;try{s=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await J(s)});
+7 -7
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bun
import{stdin as $}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as w,writeFileSync as v,existsSync as F}from"fs";import{join as x}from"path";import{homedir as H}from"os";var d="bugfix,feature,refactor,discovery,decision,change",h="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:x(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!F(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as b,mkdirSync as G}from"fs";import{join as T}from"path";var M=(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))(M||{}),p=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");b(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=M[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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}
import{stdin as $}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as w,writeFileSync as v,existsSync as F}from"fs";import{join as x}from"path";import{homedir as H}from"os";var U="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:x(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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 r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!F(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as b,mkdirSync as G}from"fs";import{join as T}from"path";var M=(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))(M||{}),p=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");b(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=M[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=M[t].padEnd(5),_=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let O="";if(n){let{sessionId:U,memorySessionId:Z,correlationId:tt,...R}=n;Object.keys(R).length>0&&(O=` {${Object.entries(R).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let D=`[${E}] [${i}] [${_}] ${a}${e}${O}${l}`;if(this.logFilePath)try{W(this.logFilePath,D+`
`,"utf8")}catch(U){process.stderr.write(`[LOGGER] Failed to write to log file: ${U}
`)}else process.stderr.write(D+`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let O="";if(n){let{sessionId:D,memorySessionId:Z,correlationId:tt,...R}=n;Object.keys(R).length>0&&(O=` {${Object.entries(R).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let C=`[${E}] [${i}] [${_}] ${a}${e}${O}${l}`;if(this.logFilePath)try{W(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+`
`)}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`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",O={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},c=new p;import L from"path";import{homedir as K}from"os";import{readFileSync as X}from"fs";var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*A.WINDOWS_MULTIPLIER):s}function N(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",O={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},c=new p;import L from"path";import{homedir as K}from"os";import{readFileSync as X}from"fs";var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*A.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
`;return i+=`To restart the worker:
`,i+=`1. Exit Claude Code completely
@@ -16,8 +16,8 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
${i}`),i}var V=L.join(K(),".claude","plugins","marketplaces","thedotmack"),Ct=I(A.HEALTH_CHECK),S=null;function u(){if(S!==null)return S;let s=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function j(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=L.join(V,"package.json");return JSON.parse(X(s,"utf-8")).version}async function Y(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=B(),t=await Y();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function y(){for(let r=0;r<75;r++){try{if(await j()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as q,existsSync as z}from"fs";function m(s,t,r=!1){if(!s||!z(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=q(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(`
${i}`),i}var V=L.join(K(),".claude","plugins","marketplaces","thedotmack"),Ct=h(A.HEALTH_CHECK),S=null;function u(){if(S!==null)return S;let s=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function j(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=L.join(V,"package.json");return JSON.parse(X(s,"utf-8")).version}async function Y(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=B(),t=await Y();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await j()){await J();return}}catch(e){c.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as q,existsSync as z}from"fs";function y(s,t,r=!1){if(!s||!z(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=q(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(`
`),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let _="",a=i.message.content;if(typeof a=="string")_=a;else if(Array.isArray(a))_=a.filter(l=>l.type==="text").map(l=>l.text).join(`
`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof a}`);return r&&(_=_.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,`
`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Q(s){if(await y(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=u();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=m(s.transcript_path,"user"),n=m(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastUserMessage:!!e,hasLastAssistantMessage:!!n});let o=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_user_message:e,last_assistant_message:n})});if(!o.ok)throw console.log(f),new Error(`Summary generation failed: ${o.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(f)}var C="";$.on("data",s=>C+=s);$.on("end",async()=>{let s;try{s=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Q(s)});
`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Q(s){if(await N(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=u();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=y(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastAssistantMessage:!!e});let n=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_assistant_message:e})});if(!n.ok)throw console.log(f),new Error(`Summary generation failed: ${n.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(f)}var m="";$.on("data",s=>m+=s);$.on("end",async()=>{let s;try{s=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Q(s)});
+6 -6
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bun
import{basename as J}from"path";import f from"path";import{homedir as H}from"os";import{readFileSync as K}from"fs";import{readFileSync as k,writeFileSync as v,existsSync as F}from"fs";import{join as w}from"path";import{homedir as W}from"os";var U="bugfix,feature,refactor,discovery,decision,change",R="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:w(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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){try{if(!F(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as b,existsSync as x,mkdirSync as G}from"fs";import{join as O}from"path";var S=(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))(S||{}),p=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=_.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");x(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=_.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=_.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${T}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=S[t].padEnd(5),T=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?`
import{basename as J}from"path";import L from"path";import{homedir as H}from"os";import{readFileSync as K}from"fs";import{readFileSync as k,writeFileSync as v,existsSync as F}from"fs";import{join as w}from"path";import{homedir as W}from"os";var U="bugfix,feature,refactor,discovery,decision,change",R="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:w(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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: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){try{if(!F(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),l.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){l.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return l.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as b,existsSync as x,mkdirSync as G}from"fs";import{join as O}from"path";var S=(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))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=_.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");x(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=_.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=_.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=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;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${T}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=S[t].padEnd(5),T=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let u="";if(n){let{sessionId:D,memorySessionId:Q,correlationId:Z,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([$,P])=>`${$}=${P}`).join(", ")}}`)}let C=`[${E}] [${i}] [${T}] ${a}${e}${u}${l}`;if(this.logFilePath)try{b(this.logFilePath,C+`
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let u="";if(n){let{sessionId:D,memorySessionId:Q,correlationId:Z,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([$,P])=>`${$}=${P}`).join(", ")}}`)}let C=`[${E}] [${i}] [${T}] ${a}${e}${u}${c}`;if(this.logFilePath)try{b(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+`
`)}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`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},c=new p;var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},h={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function I(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function d(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},l=new f;var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},h={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function I(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function d(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
`;return i+=`To restart the worker:
`,i+=`1. Exit Claude Code completely
@@ -16,7 +16,7 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
${i}`),i}var X=f.join(H(),".claude","plugins","marketplaces","thedotmack"),At=I(L.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=f.join(_.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=_.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function V(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=f.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function B(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Y(){let s=j(),t=await B();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await Y();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(d({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}await N();var y=g(),z=J(process.cwd()),A=await fetch(`http://127.0.0.1:${y}/api/context/inject?project=${encodeURIComponent(z)}&colors=true`,{method:"GET"});if(!A.ok)throw new Error(`Failed to fetch context: ${A.status}`);var q=await A.text();console.error(`
${i}`),i}var X=L.join(H(),".claude","plugins","marketplaces","thedotmack"),At=I(p.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=L.join(_.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=_.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function V(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=L.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function Y(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function B(){let s=j(),t=await Y();s!==t&&l.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await B();return}}catch(e){l.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(d({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}await N();var y=g(),z=J(process.cwd()),A=await fetch(`http://127.0.0.1:${y}/api/context/inject?project=${encodeURIComponent(z)}&colors=true`,{method:"GET"});if(!A.ok)throw new Error(`Failed to fetch context: ${A.status}`);var q=await A.text();console.error(`
\u{1F4DD} Claude-Mem Context Loaded
\u2139\uFE0F Note: This appears as stderr but is informational only
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+326
View File
@@ -2472,6 +2472,332 @@
border-color: var(--color-bg-button-hover);
}
/* Console Drawer - Chrome DevTools Style */
.console-toggle-btn {
position: fixed;
bottom: 20px;
left: 20px;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--color-bg-button);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
z-index: 999;
}
.console-toggle-btn:hover {
background: var(--color-bg-button-hover);
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.console-toggle-btn svg {
width: 20px;
height: 20px;
}
.console-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-bg-primary);
border-top: 1px solid var(--color-border-primary);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
flex-direction: column;
}
.console-resize-handle {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
cursor: ns-resize;
display: flex;
align-items: center;
justify-content: center;
}
.console-resize-handle:hover .console-resize-bar {
background: var(--color-bg-button);
}
.console-resize-bar {
width: 40px;
height: 3px;
border-radius: 2px;
background: var(--color-border-primary);
transition: background 0.2s ease;
}
.console-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border-primary);
background: var(--color-bg-header);
margin-top: 6px;
}
.console-tabs {
display: flex;
gap: 4px;
}
.console-tab {
padding: 4px 12px;
font-size: 12px;
color: var(--color-text-secondary);
background: transparent;
border: none;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.console-tab.active {
color: var(--color-text-primary);
border-bottom-color: var(--color-bg-button);
font-weight: 500;
}
.console-controls {
display: flex;
align-items: center;
gap: 8px;
}
.console-auto-refresh {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--color-text-secondary);
cursor: pointer;
user-select: none;
}
.console-auto-refresh input[type="checkbox"] {
cursor: pointer;
}
.console-control-btn {
background: transparent;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 4px 8px;
font-size: 14px;
border-radius: 4px;
transition: all 0.15s ease;
}
.console-control-btn:hover {
background: var(--color-bg-card-hover);
color: var(--color-text-primary);
}
.console-control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.console-clear-btn:hover {
color: var(--color-accent-error);
}
.console-content {
flex: 1;
overflow: auto;
background: var(--color-bg-primary);
}
.console-logs {
margin: 0;
padding: 8px 12px;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
color: var(--color-text-primary);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.console-error {
padding: 8px 12px;
background: rgba(239, 68, 68, 0.08);
border-bottom: 1px solid var(--color-accent-error);
color: var(--color-accent-error);
font-size: 11px;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
/* Console Filter Bar */
.console-filters {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 8px 12px;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-primary);
}
.console-filter-section {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.console-filter-label {
font-size: 10px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.console-filter-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.console-filter-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
font-size: 11px;
font-weight: 500;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.console-filter-chip:hover {
background: var(--color-bg-card-hover);
border-color: var(--chip-color, var(--color-border-hover));
color: var(--color-text-primary);
}
.console-filter-chip.active {
background: var(--chip-color, var(--color-accent-primary));
border-color: var(--chip-color, var(--color-accent-primary));
color: white;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
.console-filter-chip.active:hover {
opacity: 0.9;
}
.console-filter-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 12px;
background: transparent;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.15s ease;
}
.console-filter-action:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-hover);
color: var(--color-text-primary);
}
/* Log Line Styles */
.log-line {
display: block;
padding: 2px 0;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-line-raw {
color: var(--color-text-secondary);
opacity: 0.8;
}
.log-line-empty {
color: var(--color-text-muted);
font-style: italic;
padding: 20px 0;
text-align: center;
}
.log-timestamp {
color: var(--color-text-muted);
opacity: 0.7;
}
.log-level {
font-weight: 500;
}
.log-component {
font-weight: 500;
}
.log-correlation {
color: var(--color-accent-primary);
opacity: 0.9;
}
.log-message {
color: inherit;
}
/* Log Level Colors in Dark Mode */
[data-theme="dark"] .log-line-raw {
color: #8b949e;
}
/* Responsive adjustments for filter bar */
@media (max-width: 600px) {
.console-filters {
flex-direction: column;
gap: 8px;
padding: 6px 10px;
}
.console-filter-section {
flex-wrap: wrap;
}
.console-filter-chip {
padding: 2px 6px;
font-size: 10px;
}
}
/* Responsive Modal */
@media (max-width: 900px) {
.modal-body {
+137
View File
@@ -0,0 +1,137 @@
# Error Handling Anti-Pattern Rules
This folder contains `detect-error-handling-antipatterns.ts` - run it before committing any error handling changes.
## The Try-Catch Problem That Cost 10 Hours
A single overly-broad try-catch block wasted 10 hours of debugging time by silently swallowing errors.
**This pattern is BANNED.**
## BEFORE You Write Any Try-Catch
**RUN THIS TEST FIRST:**
```bash
bun run scripts/anti-pattern-test/detect-error-handling-antipatterns.ts
```
**You MUST answer these 5 questions to the user BEFORE writing try-catch:**
1. **What SPECIFIC error am I catching?** (Name the error type: `FileNotFoundError`, `NetworkTimeout`, `ValidationError`)
2. **Show documentation proving this error can occur** (Link to docs or show me the source code)
3. **Why can't this error be prevented?** (If it can be prevented, prevent it instead)
4. **What will the catch block DO?** (Must include logging + either rethrow OR explicit fallback)
5. **Why shouldn't this error propagate?** (Justify swallowing it rather than letting caller handle)
**If you cannot answer ALL 5 questions with specifics, DO NOT write the try-catch.**
## FORBIDDEN PATTERNS (Zero Tolerance)
### CRITICAL - Never Allowed
```typescript
// FORBIDDEN: Empty catch
try {
doSomething();
} catch {}
// FORBIDDEN: Catch without logging
try {
doSomething();
} catch (error) {
return null; // Silent failure!
}
// FORBIDDEN: Large try blocks (>10 lines)
try {
// 50 lines of code
// Multiple operations
// Different failure modes
} catch (error) {
logger.error('Something failed'); // Which thing?!
}
// FORBIDDEN: Promise empty catch
promise.catch(() => {}); // Error disappears into void
// FORBIDDEN: Try-catch to fix TypeScript errors
try {
// @ts-ignore
const value = response.propertyThatDoesntExist;
} catch {}
```
### ALLOWED Patterns
```typescript
// GOOD: Specific, logged, explicit handling
try {
await fetch(url);
} catch (error) {
if (error instanceof NetworkError) {
logger.warn('SYNC', 'Network request failed, will retry', { url }, error);
return null; // Explicit: null means "fetch failed"
}
throw error; // Unexpected errors propagate
}
// GOOD: Minimal scope, clear recovery
try {
JSON.parse(data);
} catch (error) {
logger.error('CONFIG', 'Corrupt settings file, using defaults', {}, error);
return DEFAULT_SETTINGS;
}
// GOOD: Fire-and-forget with logging
backgroundTask()
.catch(error => logger.warn('BACKGROUND', 'Task failed', {}, error));
// GOOD: Ignored anti-pattern for genuine hot paths only
try {
checkIfProcessAlive(pid);
} catch (error) {
// [ANTI-PATTERN IGNORED]: Tight loop checking 100s of PIDs during cleanup
return false;
}
```
## Ignoring Anti-Patterns (Rare)
**Only for genuine hot paths** where logging would cause performance problems:
```typescript
// [ANTI-PATTERN IGNORED]: Reason why logging is impossible
```
**Rules:**
- **Hot paths only** - code in tight loops called 1000s of times
- If you can add logging, ADD LOGGING - don't ignore
- Valid examples:
- "Tight loop checking process exit status during cleanup"
- "Health check polling every 100ms"
- Invalid examples:
- "Expected JSON parse failures" - Just add logger.debug
- "Common fallback path" - Just add logger.debug
## The Meta-Rule
**UNCERTAINTY TRIGGERS RESEARCH, NOT TRY-CATCH**
When you're unsure if a property exists or a method signature is correct:
1. **READ** the source code or documentation
2. **VERIFY** with the Read tool
3. **USE** TypeScript types to catch errors at compile time
4. **WRITE** code you KNOW is correct
Never use try-catch to paper over uncertainty. That wastes hours of debugging time later.
## Critical Path Protection
These files are **NEVER** allowed to have catch-and-continue:
- `SDKAgent.ts` - Errors must propagate, not hide
- `GeminiAgent.ts` - Must fail loud, not silent
- `OpenRouterAgent.ts` - Must fail loud, not silent
- `SessionStore.ts` - Database errors must propagate
- `worker-service.ts` - Core service errors must be visible
On critical paths, prefer **NO TRY-CATCH** and let errors propagate naturally.
@@ -0,0 +1,547 @@
#!/usr/bin/env bun
/**
* Error Handling Anti-Pattern Detector
*
* Detects try-catch anti-patterns that cause silent failures and debugging nightmares.
* Run this before committing code that touches error handling.
*
* Based on hard-learned lessons: defensive try-catch wastes 10+ hours of debugging time.
*/
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, relative } from 'path';
interface AntiPattern {
file: string;
line: number;
pattern: string;
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'APPROVED_OVERRIDE';
description: string;
code: string;
overrideReason?: string;
}
const CRITICAL_PATHS = [
'SDKAgent.ts',
'GeminiAgent.ts',
'OpenRouterAgent.ts',
'SessionStore.ts',
'worker-service.ts'
];
function findFilesRecursive(dir: string, pattern: RegExp): string[] {
const files: string[] = [];
const items = readdirSync(dir);
for (const item of items) {
const fullPath = join(dir, item);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
if (!item.startsWith('.') && item !== 'node_modules' && item !== 'dist' && item !== 'plugin') {
files.push(...findFilesRecursive(fullPath, pattern));
}
} else if (pattern.test(item)) {
files.push(fullPath);
}
}
return files;
}
function detectAntiPatterns(filePath: string, projectRoot: string): AntiPattern[] {
const content = readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
const antiPatterns: AntiPattern[] = [];
const relPath = relative(projectRoot, filePath);
const isCriticalPath = CRITICAL_PATHS.some(cp => filePath.includes(cp));
// Detect error message string matching for type detection (line-by-line patterns)
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Check for [ANTI-PATTERN IGNORED] on the same or previous line
const hasOverride = trimmed.includes('[ANTI-PATTERN IGNORED]') ||
(i > 0 && lines[i - 1].includes('[ANTI-PATTERN IGNORED]'));
const overrideMatch = (trimmed + (i > 0 ? lines[i - 1] : '')).match(/\[ANTI-PATTERN IGNORED\]:\s*(.+)/i);
const overrideReason = overrideMatch?.[1]?.trim();
// CRITICAL: Error message string matching for type detection
// Patterns like: errorMessage.includes('connection') or error.message.includes('timeout')
const errorStringMatchPatterns = [
/error(?:Message|\.message)\s*\.includes\s*\(\s*['"`](\w+)['"`]\s*\)/i,
/(?:err|e)\.message\s*\.includes\s*\(\s*['"`](\w+)['"`]\s*\)/i,
/String\s*\(\s*(?:error|err|e)\s*\)\s*\.includes\s*\(\s*['"`](\w+)['"`]\s*\)/i,
];
for (const pattern of errorStringMatchPatterns) {
const match = trimmed.match(pattern);
if (match) {
const matchedString = match[1];
// Common generic patterns that are too broad
const genericPatterns = ['error', 'fail', 'connection', 'timeout', 'not', 'invalid', 'unable'];
const isGeneric = genericPatterns.some(gp => matchedString.toLowerCase().includes(gp));
if (hasOverride && overrideReason) {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'ERROR_STRING_MATCHING',
severity: 'APPROVED_OVERRIDE',
description: `Error type detection via string matching on "${matchedString}" - approved override.`,
code: trimmed,
overrideReason
});
} else {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'ERROR_STRING_MATCHING',
severity: isGeneric ? 'CRITICAL' : 'HIGH',
description: `Error type detection via string matching on "${matchedString}" - fragile and masks the real error. Log the FULL error object. We don't care about pretty error handling, we care about SEEING what went wrong.`,
code: trimmed
});
}
}
}
// HIGH: Logging only error.message instead of the full error object
// Patterns like: logger.error('X', 'Y', {}, error.message) or console.error(error.message)
const partialErrorLoggingPatterns = [
/logger\.(error|warn|info|debug)\s*\([^)]*,\s*(?:error|err|e)\.message\s*\)/,
/logger\.(error|warn|info|debug)\s*\([^)]*\{\s*(?:error|err|e):\s*(?:error|err|e)\.message\s*\}/,
/console\.(error|warn|log)\s*\(\s*(?:error|err|e)\.message\s*\)/,
/console\.(error|warn|log)\s*\(\s*['"`][^'"`]+['"`]\s*,\s*(?:error|err|e)\.message\s*\)/,
];
for (const pattern of partialErrorLoggingPatterns) {
if (pattern.test(trimmed)) {
if (hasOverride && overrideReason) {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'PARTIAL_ERROR_LOGGING',
severity: 'APPROVED_OVERRIDE',
description: 'Logging only error.message instead of full error object - approved override.',
code: trimmed,
overrideReason
});
} else {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'PARTIAL_ERROR_LOGGING',
severity: 'HIGH',
description: 'Logging only error.message HIDES the stack trace, error type, and all properties. ALWAYS pass the full error object - you need the complete picture, not a summary.',
code: trimmed
});
}
}
}
// CRITICAL: Catch-all error type guessing based on message content
// Pattern: if (errorMessage.includes('X') || errorMessage.includes('Y'))
const multipleIncludes = trimmed.match(/(?:error(?:Message|\.message)|(?:err|e)\.message).*\.includes.*\|\|.*\.includes/i);
if (multipleIncludes) {
if (hasOverride && overrideReason) {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'ERROR_MESSAGE_GUESSING',
severity: 'APPROVED_OVERRIDE',
description: 'Multiple string checks on error message to guess error type - approved override.',
code: trimmed,
overrideReason
});
} else {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'ERROR_MESSAGE_GUESSING',
severity: 'CRITICAL',
description: 'Multiple string checks on error message to guess error type. STOP GUESSING. Log the FULL error object. We don\'t care what the library throws - we care about SEEING the error when it happens.',
code: trimmed
});
}
}
}
// Track try-catch blocks
let inTry = false;
let tryStartLine = 0;
let tryLines: string[] = [];
let braceDepth = 0;
let catchStartLine = 0;
let catchLines: string[] = [];
let inCatch = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Detect standalone promise empty catch: .catch(() => {})
const emptyPromiseCatch = trimmed.match(/\.catch\s*\(\s*\(\s*\)\s*=>\s*\{\s*\}\s*\)/);
if (emptyPromiseCatch) {
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'PROMISE_EMPTY_CATCH',
severity: 'CRITICAL',
description: 'Promise .catch() with empty handler - errors disappear into the void.',
code: trimmed
});
}
// Detect standalone promise catch without logging: .catch(err => ...)
const promiseCatchMatch = trimmed.match(/\.catch\s*\(\s*(?:\(\s*)?(\w+)(?:\s*\))?\s*=>/);
if (promiseCatchMatch && !emptyPromiseCatch) {
// Look ahead up to 10 lines to see if there's logging in the handler body
let catchBody = trimmed.substring(promiseCatchMatch.index || 0);
let braceCount = (catchBody.match(/{/g) || []).length - (catchBody.match(/}/g) || []).length;
// Collect subsequent lines if the handler spans multiple lines
let lookAhead = 0;
while (braceCount > 0 && lookAhead < 10 && i + lookAhead + 1 < lines.length) {
lookAhead++;
const nextLine = lines[i + lookAhead];
catchBody += '\n' + nextLine;
braceCount += (nextLine.match(/{/g) || []).length - (nextLine.match(/}/g) || []).length;
}
const hasLogging = catchBody.match(/logger\.(error|warn|debug|info|failure)/) ||
catchBody.match(/console\.(error|warn)/);
if (!hasLogging && lookAhead > 0) { // Only flag if it's actually a multi-line handler
antiPatterns.push({
file: relPath,
line: i + 1,
pattern: 'PROMISE_CATCH_NO_LOGGING',
severity: 'CRITICAL',
description: 'Promise .catch() without logging - errors are silently swallowed.',
code: catchBody.trim().split('\n').slice(0, 5).join('\n')
});
}
}
// Detect try block start
if (trimmed.match(/^\s*try\s*{/) || trimmed.match(/}\s*try\s*{/)) {
inTry = true;
tryStartLine = i + 1;
tryLines = [line];
braceDepth = 1;
continue;
}
// Track try block content
if (inTry && !inCatch) {
tryLines.push(line);
// Count braces to find try block end
const openBraces = (line.match(/{/g) || []).length;
const closeBraces = (line.match(/}/g) || []).length;
braceDepth += openBraces - closeBraces;
// Found catch
if (trimmed.match(/}\s*catch\s*(\(|{)/)) {
inCatch = true;
catchStartLine = i + 1;
catchLines = [line];
braceDepth = 1;
continue;
}
}
// Track catch block
if (inCatch) {
catchLines.push(line);
const openBraces = (line.match(/{/g) || []).length;
const closeBraces = (line.match(/}/g) || []).length;
braceDepth += openBraces - closeBraces;
// Catch block ended
if (braceDepth === 0) {
// Analyze the try-catch block
analyzeTryCatchBlock(
filePath,
relPath,
tryStartLine,
tryLines,
catchStartLine,
catchLines,
isCriticalPath,
antiPatterns
);
// Reset
inTry = false;
inCatch = false;
tryLines = [];
catchLines = [];
}
}
}
return antiPatterns;
}
function analyzeTryCatchBlock(
filePath: string,
relPath: string,
tryStartLine: number,
tryLines: string[],
catchStartLine: number,
catchLines: string[],
isCriticalPath: boolean,
antiPatterns: AntiPattern[]
): void {
const tryBlock = tryLines.join('\n');
const catchBlock = catchLines.join('\n');
// CRITICAL: Empty catch block
const catchContent = catchBlock
.replace(/}\s*catch\s*\([^)]*\)\s*{/, '') // Remove catch signature
.replace(/}\s*catch\s*{/, '') // Remove catch without param
.replace(/}$/, '') // Remove closing brace
.trim();
// Check for comment-only catch blocks
const nonCommentContent = catchContent
.split('\n')
.filter(line => {
const t = line.trim();
return t && !t.startsWith('//') && !t.startsWith('/*') && !t.startsWith('*');
})
.join('\n')
.trim();
if (!nonCommentContent || nonCommentContent === '') {
antiPatterns.push({
file: relPath,
line: catchStartLine,
pattern: 'EMPTY_CATCH',
severity: 'CRITICAL',
description: 'Empty catch block - errors are silently swallowed. User will waste hours debugging.',
code: catchBlock.trim()
});
}
// Check for [ANTI-PATTERN IGNORED] marker
const overrideMatch = catchContent.match(/\/\/\s*\[ANTI-PATTERN IGNORED\]:\s*(.+)/i);
const overrideReason = overrideMatch?.[1]?.trim();
// CRITICAL: No logging in catch block (unless explicitly approved)
const hasLogging = catchContent.match(/logger\.(error|warn|debug|info|failure)/);
const hasConsoleError = catchContent.match(/console\.(error|warn)/);
const hasStderr = catchContent.match(/process\.stderr\.write/);
const hasThrow = catchContent.match(/throw/);
if (!hasLogging && !hasConsoleError && !hasStderr && !hasThrow && nonCommentContent) {
if (overrideReason) {
antiPatterns.push({
file: relPath,
line: catchStartLine,
pattern: 'NO_LOGGING_IN_CATCH',
severity: 'APPROVED_OVERRIDE',
description: 'Catch block has no logging - approved override.',
code: catchBlock.trim(),
overrideReason
});
} else {
antiPatterns.push({
file: relPath,
line: catchStartLine,
pattern: 'NO_LOGGING_IN_CATCH',
severity: 'CRITICAL',
description: 'Catch block has no logging - errors occur invisibly.',
code: catchBlock.trim()
});
}
}
// HIGH: Large try block (>10 lines)
const significantTryLines = tryLines.filter(line => {
const t = line.trim();
return t && !t.startsWith('//') && t !== '{' && t !== '}';
}).length;
if (significantTryLines > 10) {
antiPatterns.push({
file: relPath,
line: tryStartLine,
pattern: 'LARGE_TRY_BLOCK',
severity: 'HIGH',
description: `Try block has ${significantTryLines} lines - too broad. Multiple errors lumped together.`,
code: `${tryLines.slice(0, 3).join('\n')}\n... (${significantTryLines} lines) ...`
});
}
// HIGH: Generic catch without type checking
const catchParam = catchBlock.match(/catch\s*\(([^)]+)\)/)?.[1]?.trim();
const hasTypeCheck = catchContent.match(/instanceof\s+Error/) ||
catchContent.match(/\.name\s*===/) ||
catchContent.match(/typeof.*===\s*['"]object['"]/);
if (catchParam && !hasTypeCheck && nonCommentContent) {
antiPatterns.push({
file: relPath,
line: catchStartLine,
pattern: 'GENERIC_CATCH',
severity: 'MEDIUM',
description: 'Catch block handles all errors identically - no error type discrimination.',
code: catchBlock.trim()
});
}
// CRITICAL on critical paths: Catch-and-continue
if (isCriticalPath && nonCommentContent && !hasThrow) {
const hasReturn = catchContent.match(/return/);
const hasProcessExit = catchContent.match(/process\.exit/);
const terminatesExecution = hasReturn || hasProcessExit;
if (!terminatesExecution && hasLogging) {
if (overrideReason) {
antiPatterns.push({
file: relPath,
line: catchStartLine,
pattern: 'CATCH_AND_CONTINUE_CRITICAL_PATH',
severity: 'APPROVED_OVERRIDE',
description: 'Critical path continues after error - anti-pattern ignored.',
code: catchBlock.trim(),
overrideReason
});
} else {
antiPatterns.push({
file: relPath,
line: catchStartLine,
pattern: 'CATCH_AND_CONTINUE_CRITICAL_PATH',
severity: 'CRITICAL',
description: 'Critical path continues after error - may cause silent data corruption.',
code: catchBlock.trim()
});
}
}
}
}
function formatReport(antiPatterns: AntiPattern[]): string {
const critical = antiPatterns.filter(a => a.severity === 'CRITICAL');
const high = antiPatterns.filter(a => a.severity === 'HIGH');
const medium = antiPatterns.filter(a => a.severity === 'MEDIUM');
const approved = antiPatterns.filter(a => a.severity === 'APPROVED_OVERRIDE');
if (antiPatterns.length === 0) {
return '✅ No error handling anti-patterns detected!\n';
}
let report = '\n';
report += '═══════════════════════════════════════════════════════════════\n';
report += ' ERROR HANDLING ANTI-PATTERNS DETECTED\n';
report += '═══════════════════════════════════════════════════════════════\n\n';
report += `Found ${critical.length + high.length + medium.length} anti-patterns:\n`;
report += ` 🔴 CRITICAL: ${critical.length}\n`;
report += ` 🟠 HIGH: ${high.length}\n`;
report += ` 🟡 MEDIUM: ${medium.length}\n`;
if (approved.length > 0) {
report += ` ⚪ APPROVED OVERRIDES: ${approved.length}\n`;
}
report += '\n';
if (critical.length > 0) {
report += '🔴 CRITICAL ISSUES (Fix immediately - these cause silent failures):\n';
report += '─────────────────────────────────────────────────────────────\n\n';
for (const ap of critical) {
report += `📁 ${ap.file}:${ap.line}\n`;
report += `${ap.pattern}\n`;
report += ` ${ap.description}\n\n`;
report += ` Code:\n`;
const codeLines = ap.code.split('\n');
for (const line of codeLines.slice(0, 5)) {
report += ` ${line}\n`;
}
if (codeLines.length > 5) {
report += ` ... (${codeLines.length - 5} more lines)\n`;
}
report += '\n';
}
}
if (high.length > 0) {
report += '🟠 HIGH PRIORITY:\n';
report += '─────────────────────────────────────────────────────────────\n\n';
for (const ap of high) {
report += `📁 ${ap.file}:${ap.line} - ${ap.pattern}\n`;
report += ` ${ap.description}\n\n`;
}
}
if (medium.length > 0) {
report += '🟡 MEDIUM PRIORITY:\n';
report += '─────────────────────────────────────────────────────────────\n\n';
for (const ap of medium) {
report += `📁 ${ap.file}:${ap.line} - ${ap.pattern}\n`;
report += ` ${ap.description}\n\n`;
}
}
if (approved.length > 0) {
report += '⚪ APPROVED OVERRIDES (Review reasons for accuracy):\n';
report += '─────────────────────────────────────────────────────────────\n\n';
for (const ap of approved) {
report += `📁 ${ap.file}:${ap.line} - ${ap.pattern}\n`;
report += ` Reason: ${ap.overrideReason}\n`;
report += ` Code:\n`;
const codeLines = ap.code.split('\n');
for (const line of codeLines.slice(0, 3)) {
report += ` ${line}\n`;
}
if (codeLines.length > 3) {
report += ` ... (${codeLines.length - 3} more lines)\n`;
}
report += '\n';
}
}
report += '═══════════════════════════════════════════════════════════════\n';
report += 'REMINDER: Every try-catch must answer these questions:\n';
report += '1. What SPECIFIC error am I catching? (Name it)\n';
report += '2. Show me documentation proving this error can occur\n';
report += '3. Why can\'t this error be prevented?\n';
report += '4. What will the catch block DO? (Log + rethrow? Fallback?)\n';
report += '5. Why shouldn\'t this error propagate to the caller?\n';
report += '\n';
report += 'To ignore an anti-pattern, add: // [ANTI-PATTERN IGNORED]: reason\n';
report += '═══════════════════════════════════════════════════════════════\n\n';
return report;
}
// Main execution
const projectRoot = process.cwd();
const srcDir = join(projectRoot, 'src');
console.log('🔍 Scanning for error handling anti-patterns...\n');
const tsFiles = findFilesRecursive(srcDir, /\.ts$/);
console.log(`Found ${tsFiles.length} TypeScript files\n`);
let allAntiPatterns: AntiPattern[] = [];
for (const file of tsFiles) {
const patterns = detectAntiPatterns(file, projectRoot);
allAntiPatterns = allAntiPatterns.concat(patterns);
}
const report = formatReport(allAntiPatterns);
console.log(report);
// Exit with error code if critical issues found
const critical = allAntiPatterns.filter(a => a.severity === 'CRITICAL');
if (critical.length > 0) {
console.error(`❌ FAILED: ${critical.length} critical error handling anti-patterns must be fixed.\n`);
process.exit(1);
}
process.exit(0);
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env bun
/**
* Clear messages from the queue
*
* Usage:
* bun scripts/clear-failed-queue.ts # Clear failed messages (interactive)
* bun scripts/clear-failed-queue.ts --all # Clear ALL messages (pending, processing, failed)
* bun scripts/clear-failed-queue.ts --force # Non-interactive - clear without prompting
*/
const WORKER_URL = 'http://localhost:37777';
interface QueueMessage {
id: number;
session_db_id: number;
message_type: string;
tool_name: string | null;
status: 'pending' | 'processing' | 'failed';
retry_count: number;
created_at_epoch: number;
project: string | null;
}
interface QueueResponse {
queue: {
messages: QueueMessage[];
totalPending: number;
totalProcessing: number;
totalFailed: number;
stuckCount: number;
};
recentlyProcessed: QueueMessage[];
sessionsWithPendingWork: number[];
}
interface ClearResponse {
success: boolean;
clearedCount: number;
}
async function checkWorkerHealth(): Promise<boolean> {
try {
const res = await fetch(`${WORKER_URL}/api/health`);
return res.ok;
} catch {
return false;
}
}
async function getQueueStatus(): Promise<QueueResponse> {
const res = await fetch(`${WORKER_URL}/api/pending-queue`);
if (!res.ok) {
throw new Error(`Failed to get queue status: ${res.status}`);
}
return res.json();
}
async function clearFailedQueue(): Promise<ClearResponse> {
const res = await fetch(`${WORKER_URL}/api/pending-queue/failed`, {
method: 'DELETE'
});
if (!res.ok) {
throw new Error(`Failed to clear failed queue: ${res.status}`);
}
return res.json();
}
async function clearAllQueue(): Promise<ClearResponse> {
const res = await fetch(`${WORKER_URL}/api/pending-queue/all`, {
method: 'DELETE'
});
if (!res.ok) {
throw new Error(`Failed to clear queue: ${res.status}`);
}
return res.json();
}
function formatAge(epochMs: number): string {
const ageMs = Date.now() - epochMs;
const minutes = Math.floor(ageMs / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h ago`;
if (hours > 0) return `${hours}h ${minutes % 60}m ago`;
return `${minutes}m ago`;
}
async function prompt(question: string): Promise<string> {
// Check if we have a TTY for interactive input
if (!process.stdin.isTTY) {
console.log(question + '(no TTY, use --force flag for non-interactive mode)');
return 'n';
}
return new Promise((resolve) => {
process.stdout.write(question);
process.stdin.setRawMode(false);
process.stdin.resume();
process.stdin.once('data', (data) => {
process.stdin.pause();
resolve(data.toString().trim());
});
});
}
async function main() {
const args = process.argv.slice(2);
// Help flag
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Claude-Mem Queue Clearer
Clear messages from the observation queue.
Usage:
bun scripts/clear-failed-queue.ts [options]
Options:
--help, -h Show this help message
--all Clear ALL messages (pending, processing, and failed)
--force Clear without prompting for confirmation
Examples:
# Clear failed messages interactively
bun scripts/clear-failed-queue.ts
# Clear ALL messages (pending, processing, failed)
bun scripts/clear-failed-queue.ts --all
# Clear without confirmation (non-interactive)
bun scripts/clear-failed-queue.ts --force
# Clear all messages without confirmation
bun scripts/clear-failed-queue.ts --all --force
What is this for?
Failed messages are observations that exceeded the maximum retry count.
Processing/pending messages may be stuck or unwanted.
This command removes them to clean up the queue.
--all is useful for a complete reset when you want to start fresh.
`);
process.exit(0);
}
const force = args.includes('--force');
const clearAll = args.includes('--all');
console.log(clearAll
? '\n=== Claude-Mem Queue Clearer (ALL) ===\n'
: '\n=== Claude-Mem Queue Clearer (Failed) ===\n');
// Check worker health
const healthy = await checkWorkerHealth();
if (!healthy) {
console.log('Worker is not running. Start it with:');
console.log(' cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:start\n');
process.exit(1);
}
console.log('Worker status: Running\n');
// Get queue status
const status = await getQueueStatus();
const { queue } = status;
console.log('Queue Summary:');
console.log(` Pending: ${queue.totalPending}`);
console.log(` Processing: ${queue.totalProcessing}`);
console.log(` Failed: ${queue.totalFailed}`);
console.log('');
// Check if there are messages to clear
const totalToClear = clearAll
? queue.totalPending + queue.totalProcessing + queue.totalFailed
: queue.totalFailed;
if (totalToClear === 0) {
console.log(clearAll
? 'No messages in queue. Nothing to clear.\n'
: 'No failed messages in queue. Nothing to clear.\n');
process.exit(0);
}
// Show details about messages to clear
const messagesToShow = clearAll ? queue.messages : queue.messages.filter(m => m.status === 'failed');
if (messagesToShow.length > 0) {
console.log(clearAll ? 'Messages to Clear:' : 'Failed Messages:');
console.log('─'.repeat(80));
// Group by session
const bySession = new Map<number, QueueMessage[]>();
for (const msg of messagesToShow) {
const list = bySession.get(msg.session_db_id) || [];
list.push(msg);
bySession.set(msg.session_db_id, list);
}
for (const [sessionId, messages] of bySession) {
const project = messages[0].project || 'unknown';
const oldest = Math.min(...messages.map(m => m.created_at_epoch));
if (clearAll) {
const statuses = {
pending: messages.filter(m => m.status === 'pending').length,
processing: messages.filter(m => m.status === 'processing').length,
failed: messages.filter(m => m.status === 'failed').length
};
console.log(` Session ${sessionId} (${project})`);
console.log(` Messages: ${messages.length} total (${statuses.pending} pending, ${statuses.processing} processing, ${statuses.failed} failed)`);
console.log(` Age: ${formatAge(oldest)}`);
} else {
console.log(` Session ${sessionId} (${project})`);
console.log(` Messages: ${messages.length} failed`);
console.log(` Age: ${formatAge(oldest)}`);
}
}
console.log('─'.repeat(80));
console.log('');
}
// Confirm before clearing
const clearMessage = clearAll
? `Clear ${totalToClear} messages (pending, processing, and failed)?`
: `Clear ${queue.totalFailed} failed messages?`;
if (force) {
console.log(`${clearMessage.replace('?', '')}...\n`);
} else {
const answer = await prompt(`${clearMessage} [y/N]: `);
if (answer.toLowerCase() !== 'y') {
console.log('\nCancelled. Run with --force to skip confirmation.\n');
process.exit(0);
}
console.log('');
}
// Clear the queue
const result = clearAll ? await clearAllQueue() : await clearFailedQueue();
console.log('Clearing Result:');
console.log(` Messages cleared: ${result.clearedCount}`);
console.log(` Status: ${result.success ? 'Success' : 'Failed'}\n`);
if (result.success && result.clearedCount > 0) {
console.log(clearAll
? 'All messages have been removed from the queue.\n'
: 'Failed messages have been removed from the queue.\n');
}
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
+8 -2
View File
@@ -8,6 +8,7 @@ import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { logger } from '../utils/logger.js';
interface ObservationData {
type: string;
@@ -56,7 +57,8 @@ function buildTimestampMap(): TimestampMapping {
const content = readFileSync(filepath, 'utf-8');
const lines = content.split('\n').filter(l => l.trim());
for (const line of lines) {
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
try {
const data = JSON.parse(line);
const timestamp = data.timestamp;
@@ -75,7 +77,11 @@ function buildTimestampMap(): TimestampMapping {
}
}
} catch (e) {
// Skip invalid JSON lines
logger.debug('IMPORT', 'Skipping invalid JSON line', {
lineNumber: index + 1,
filename,
error: e instanceof Error ? e.message : String(e)
});
}
}
}
+3 -4
View File
@@ -42,13 +42,13 @@ async function summaryHook(input?: StopInput): Promise<void> {
throw new Error(`Missing transcript_path in Stop hook input for session ${session_id}`);
}
// Extract last user AND assistant messages from transcript
const lastUserMessage = extractLastMessage(input.transcript_path, 'user');
// Extract last assistant message from transcript (the work Claude did)
// Note: "user" messages in transcripts are mostly tool_results, not actual user input.
// The user's original request is already stored in user_prompts table.
const lastAssistantMessage = extractLastMessage(input.transcript_path, 'assistant', true);
logger.dataIn('HOOK', 'Stop: Requesting summary', {
workerPort: port,
hasLastUserMessage: !!lastUserMessage,
hasLastAssistantMessage: !!lastAssistantMessage
});
@@ -58,7 +58,6 @@ async function summaryHook(input?: StopInput): Promise<void> {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: session_id,
last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
+10 -5
View File
@@ -20,7 +20,6 @@ export interface SDKSession {
memory_session_id: string | null;
project: string;
user_prompt: string;
last_user_message?: string;
last_assistant_message?: string;
}
@@ -96,14 +95,20 @@ export function buildObservationPrompt(obs: Observation): string {
try {
toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
} catch {
toolInput = obs.tool_input; // If parse fails, use raw value
} catch (error) {
logger.debug('SDK', 'Tool input is plain string, using as-is', {
toolName: obs.tool_name
}, error as Error);
toolInput = obs.tool_input;
}
try {
toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
} catch {
toolOutput = obs.tool_output; // If parse fails, use raw value
} catch (error) {
logger.debug('SDK', 'Tool output is plain string, using as-is', {
toolName: obs.tool_name
}, error as Error);
toolOutput = obs.tool_output;
}
return `<observed_from_primary_session>
+3
View File
@@ -140,6 +140,8 @@ async function verifyWorkerConnection(): Promise<boolean> {
const response = await fetch(`${WORKER_BASE_URL}/api/health`);
return response.ok;
} catch (error) {
// Expected during worker startup or if worker is down
logger.debug('SYSTEM', 'Worker health check failed', undefined, { error: error instanceof Error ? error.message : String(error) });
return false;
}
}
@@ -265,6 +267,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
return await tool.handler(request.params.arguments || {});
} catch (error: any) {
logger.error('SYSTEM', 'Tool execution failed', undefined, { tool: request.params.name, error: error.message });
return {
content: [{
type: 'text' as const,
+2 -1
View File
@@ -200,6 +200,7 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
}
}
} catch (parseError) {
logger.debug('PARSER', 'Skipping malformed transcript line', { lineIndex: i }, parseError as Error);
continue;
}
}
@@ -227,7 +228,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
try {
unlinkSync(VERSION_MARKER_PATH);
} catch (unlinkError) {
// Marker might not exist
logger.debug('SYSTEM', 'Marker file cleanup failed (may not exist)', {}, unlinkError as Error);
}
logger.error('SYSTEM', 'Native module rebuild needed - restart Claude Code to auto-fix');
return '';
+29 -5
View File
@@ -14,7 +14,6 @@ export interface PersistentPendingMessage {
tool_input: string | null;
tool_response: string | null;
cwd: string | null;
last_user_message: string | null;
last_assistant_message: string | null;
prompt_number: number | null;
status: 'pending' | 'processing' | 'processed' | 'failed';
@@ -59,9 +58,9 @@ export class PendingMessageStore {
INSERT INTO pending_messages (
session_db_id, content_session_id, message_type,
tool_name, tool_input, tool_response, cwd,
last_user_message, last_assistant_message,
last_assistant_message,
prompt_number, status, retry_count, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
`);
const result = stmt.run(
@@ -72,7 +71,6 @@ export class PendingMessageStore {
message.tool_input ? JSON.stringify(message.tool_input) : null,
message.tool_response ? JSON.stringify(message.tool_response) : null,
message.cwd || null,
message.last_user_message || null,
message.last_assistant_message || null,
message.prompt_number || null,
now
@@ -384,6 +382,33 @@ export class PendingMessageStore {
return result.changes;
}
/**
* Clear all failed messages from the queue
* @returns Number of messages deleted
*/
clearFailed(): number {
const stmt = this.db.prepare(`
DELETE FROM pending_messages
WHERE status = 'failed'
`);
const result = stmt.run();
return result.changes;
}
/**
* Clear all pending, processing, and failed messages from the queue
* Keeps only processed messages (for history)
* @returns Number of messages deleted
*/
clearAll(): number {
const stmt = this.db.prepare(`
DELETE FROM pending_messages
WHERE status IN ('pending', 'processing', 'failed')
`);
const result = stmt.run();
return result.changes;
}
/**
* Convert a PersistentPendingMessage back to PendingMessage format
*/
@@ -395,7 +420,6 @@ export class PendingMessageStore {
tool_response: persistent.tool_response ? JSON.parse(persistent.tool_response) : undefined,
prompt_number: persistent.prompt_number || undefined,
cwd: persistent.cwd || undefined,
last_user_message: persistent.last_user_message || undefined,
last_assistant_message: persistent.last_assistant_message || undefined
};
}
+4 -1
View File
@@ -165,7 +165,10 @@ export class ChromaSync {
logger.debug('CHROMA_SYNC', 'Collection exists', { collection: this.collectionName });
} catch (error) {
// Collection doesn't exist, create it
// Log the FULL error - don't try to guess what type it is
logger.warn('CHROMA_SYNC', 'Collection check failed, attempting to create', { collection: this.collectionName }, error as Error);
// Try to create collection - if this also fails, we'll see that error too
logger.info('CHROMA_SYNC', 'Creating collection', { collection: this.collectionName });
try {
+49 -37
View File
@@ -66,7 +66,8 @@ function removePidFile(): void {
try {
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
} catch (error) {
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE, error: (error as Error).message });
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE }, error as Error);
return; // Non-critical cleanup, OK to fail
}
}
@@ -128,7 +129,8 @@ export async function updateCursorContextForProject(projectName: string, port: n
writeContextFile(entry.workspacePath, context);
logger.debug('CURSOR', 'Updated context file', { projectName, workspacePath: entry.workspacePath });
} catch (error) {
logger.warn('CURSOR', 'Failed to update context file', { projectName, error: (error as Error).message });
logger.warn('CURSOR', 'Failed to update context file', { projectName }, error as Error);
return; // Non-critical context update, OK to fail
}
}
@@ -147,7 +149,10 @@ async function isPortInUse(port: number): Promise<boolean> {
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
return response.ok;
} catch { return false; }
} catch (error) {
// [ANTI-PATTERN IGNORED]: Health check polls every 500ms, logging would flood
return false;
}
}
async function waitForHealth(port: number, timeoutMs: number = 30000): Promise<boolean> {
@@ -157,8 +162,9 @@ async function waitForHealth(port: number, timeoutMs: number = 30000): Promise<b
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`);
if (response.ok) return true;
} catch {
// Not ready yet
} catch (error) {
// [ANTI-PATTERN IGNORED]: Retry loop - expected failures during startup, will retry
logger.debug('SYSTEM', 'Service not ready yet, will retry', { port }, error as Error);
}
await new Promise(r => setTimeout(r, 500));
}
@@ -215,6 +221,8 @@ async function getRunningWorkerVersion(port: number): Promise<string | null> {
const data = await response.json() as { version: string };
return data.version;
} catch {
// Expected: worker not running or version endpoint unavailable
logger.debug('SYSTEM', 'Could not fetch worker version', { port });
return null;
}
}
@@ -256,6 +264,7 @@ import { SessionRoutes } from './worker/http/routes/SessionRoutes.js';
import { DataRoutes } from './worker/http/routes/DataRoutes.js';
import { SearchRoutes } from './worker/http/routes/SearchRoutes.js';
import { SettingsRoutes } from './worker/http/routes/SettingsRoutes.js';
import { LogsRoutes } from './worker/http/routes/LogsRoutes.js';
export class WorkerService {
private app: express.Application;
@@ -285,6 +294,7 @@ export class WorkerService {
private dataRoutes: DataRoutes;
private searchRoutes: SearchRoutes | null;
private settingsRoutes: SettingsRoutes;
private logsRoutes: LogsRoutes;
// Initialization tracking
private initializationComplete: Promise<void>;
@@ -329,6 +339,7 @@ export class WorkerService {
// SearchRoutes needs SearchManager which requires initialized DB - will be created in initializeBackground()
this.searchRoutes = null;
this.settingsRoutes = new SettingsRoutes(this.settingsManager);
this.logsRoutes = new LogsRoutes();
this.setupMiddleware();
this.setupRoutes();
@@ -356,7 +367,7 @@ export class WorkerService {
process.exit(0);
} catch (error) {
logger.error('SYSTEM', 'Error during shutdown', {}, error as Error);
process.exit(1);
process.exit(1); // Exit with error code - this terminates execution
}
};
@@ -445,6 +456,7 @@ export class WorkerService {
}]
});
} catch (error) {
// [POSSIBLY RELEVANT]: API must respond even on error, log full error and return error response
logger.error('WORKER', 'Failed to load instructions', { topic, operation }, error as Error);
res.status(500).json({
content: [{
@@ -503,6 +515,7 @@ export class WorkerService {
this.dataRoutes.setupRoutes(this.app);
// searchRoutes is set up after database initialization in initializeBackground()
this.settingsRoutes.setupRoutes(this.app);
this.logsRoutes.setupRoutes(this.app);
// Register early handler for /api/context/inject to avoid 404 during startup
// This handler waits for initialization to complete before delegating to SearchRoutes
@@ -528,6 +541,7 @@ export class WorkerService {
// This avoids code duplication and "headers already sent" errors
next();
} catch (error) {
// [POSSIBLY RELEVANT]: API must respond even on error, log full error and return error response
logger.error('WORKER', 'Context inject handler failed', {}, error as Error);
if (!res.headersSent) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error' });
@@ -605,16 +619,18 @@ export class WorkerService {
}
try {
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 60000, stdio: 'ignore' });
} catch {
// Process may have already exited - continue cleanup
} catch (error) {
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', { pid }, error as Error);
}
}
} else {
for (const pid of pids) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Process already exited - that's fine
} catch (error) {
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
logger.debug('SYSTEM', 'Process already exited', { pid }, error as Error);
}
}
}
@@ -747,25 +763,15 @@ export class WorkerService {
session.generatorPromise = this.sdkAgent.startSession(session, this)
.catch(error => {
// Only log if not aborted
if (session.abortController.signal.aborted) return;
logger.error('SYSTEM', `Generator failed (${source})`, {
sessionId: sid,
error: error.message
}, error);
logger.error('SDK', 'Session generator failed', {
sessionId: session.sessionDbId,
project: session.project
}, error as Error);
// Note: Error is logged but not rethrown - session marked as complete via finally
})
.finally(() => {
session.generatorPromise = null;
this.broadcastProcessingStatus();
// Crash recovery: if not aborted, check if we should restart
if (!session.abortController.signal.aborted) {
// We can check if there are pending messages to decide if restart is urgent
// But generally, if it crashed, we might want to restart?
// For now, let's just log. The user/system can trigger restart if needed.
logger.warn('SYSTEM', `Session processor exited unexpectedly`, { sessionId: sid });
}
});
}
@@ -829,6 +835,7 @@ export class WorkerService {
// Small delay between sessions to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
// [ANTI-PATTERN IGNORED]: Recovery loop - skip failed session, continue to next
logger.warn('SYSTEM', `Failed to process session ${sessionDbId}`, {}, error as Error);
result.sessionsSkipped++;
}
@@ -976,9 +983,9 @@ export class WorkerService {
process.kill(pid, 'SIGKILL');
}
logger.info('SYSTEM', 'Killed process', { pid });
} catch {
// Process may have already exited - continue shutdown
logger.debug('SYSTEM', 'Process already exited during force kill', { pid });
} catch (error) {
// [ANTI-PATTERN IGNORED]: Shutdown cleanup - process already exited, continue
logger.debug('SYSTEM', 'Process already exited during force kill', { pid }, error as Error);
}
}
@@ -993,7 +1000,8 @@ export class WorkerService {
try {
process.kill(pid, 0);
return true;
} catch {
} catch (error) {
// [ANTI-PATTERN IGNORED]: Tight loop checking 100s of PIDs every 100ms during cleanup
return false;
}
});
@@ -1082,8 +1090,9 @@ async function runInteractiveSetup(): Promise<number> {
if (existsSync(settingsPath)) {
try {
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
} catch {
// Start fresh if corrupt
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - corrupt settings, continue with defaults
logger.debug('SETUP', 'Corrupt settings file, starting fresh', { path: settingsPath }, error as Error);
}
}
@@ -1288,8 +1297,9 @@ async function detectClaudeCode(): Promise<boolean> {
if (stdout.trim()) {
return true;
}
} catch {
// CLI not found
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - CLI not found, continue to directory check
logger.debug('SYSTEM', 'Claude CLI not in PATH', {}, error as Error);
}
// Check for Claude Code plugin directory
@@ -1400,8 +1410,9 @@ function configureCursorMcp(target: string): number {
if (!config.mcpServers) {
config.mcpServers = {};
}
} catch {
// Start fresh if corrupt
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - corrupt config, continue with empty
logger.warn('SYSTEM', 'Corrupt mcp.json, creating new config', { path: mcpJsonPath }, error as Error);
config = { mcpServers: {} };
}
}
@@ -1656,8 +1667,9 @@ ${context}
}
}
}
} catch {
// Worker not running - that's ok, context will be generated after first session
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - worker not running, use placeholder
logger.debug('CURSOR', 'Worker not running during install', {}, error as Error);
}
if (!contextGenerated) {
-1
View File
@@ -43,7 +43,6 @@ export interface PendingMessage {
tool_response?: any;
prompt_number?: number;
cwd?: string;
last_user_message?: string;
last_assistant_message?: string;
}
+5 -3
View File
@@ -204,8 +204,9 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
logger.debug('BRANCH', 'Checking out branch', { branch: targetBranch });
try {
execGit(['checkout', targetBranch]);
} catch {
} catch (error) {
// Branch might not exist locally, try tracking remote
logger.debug('BRANCH', 'Branch not local, tracking remote', { branch: targetBranch, error: error instanceof Error ? error.message : String(error) });
execGit(['checkout', '-b', targetBranch, `origin/${targetBranch}`]);
}
@@ -239,8 +240,9 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
if (info.branch && isValidBranchName(info.branch)) {
execGit(['checkout', info.branch]);
}
} catch {
// Recovery failed, user needs manual intervention
} catch (recoveryError) {
// [POSSIBLY RELEVANT]: Recovery checkout failed, user needs manual intervention - already logging main error above
logger.warn('BRANCH', 'Recovery checkout also failed', { originalBranch: info.branch }, recoveryError as Error);
}
return {
+4 -3
View File
@@ -229,7 +229,6 @@ export class GeminiAgent {
memory_session_id: session.memorySessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
}, mode);
@@ -495,9 +494,11 @@ export class GeminiAgent {
}
});
}
// Update Cursor context file for registered projects (fire-and-forget)
updateCursorContextForProject(session.project, getWorkerPort()).catch(() => {});
updateCursorContextForProject(session.project, getWorkerPort()).catch(error => {
logger.warn('CURSOR', 'Context update failed (non-critical)', { project: session.project }, error as Error);
});
}
// Mark messages as processed
+4 -3
View File
@@ -188,7 +188,6 @@ export class OpenRouterAgent {
memory_session_id: session.memorySessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
}, mode);
@@ -538,9 +537,11 @@ export class OpenRouterAgent {
}
});
}
// Update Cursor context file for registered projects (fire-and-forget)
updateCursorContextForProject(session.project, getWorkerPort()).catch(() => {});
updateCursorContextForProject(session.project, getWorkerPort()).catch(error => {
logger.warn('CURSOR', 'Context update failed (non-critical)', { project: session.project }, error as Error);
});
}
// Mark messages as processed
+2 -2
View File
@@ -51,8 +51,8 @@ export class PaginationHelper {
// Return as JSON string
return JSON.stringify(strippedPaths);
} catch (error) {
// If parsing fails, return original string
} catch (err) {
logger.debug('WORKER', 'File paths is plain string, using as-is', {}, err as Error);
return filePathsStr;
}
}
+11 -16
View File
@@ -41,7 +41,10 @@ export class SDKAgent {
* @param worker WorkerService reference for spinner control (optional)
*/
async startSession(session: ActiveSession, worker?: any): Promise<void> {
try {
// Find Claude executable
const claudePath = this.findClaudeExecutable();
@@ -183,18 +186,8 @@ export class SDKAgent {
duration: `${(sessionDuration / 1000).toFixed(1)}s`
});
} catch (error: any) {
if (error.name === 'AbortError') {
logger.warn('SDK', 'Agent aborted', { sessionId: session.sessionDbId });
} else {
logger.failure('SDK', 'Agent error', { sessionDbId: session.sessionDbId }, error);
}
throw error;
} finally {
// NOTE: Do NOT delete session here - SessionRoutes.finally() handles cleanup
// and auto-restart logic. Deleting here races with pending work checks.
}
}
/**
* Create event-driven message generator (yields messages from SessionManager)
@@ -296,7 +289,6 @@ export class SDKAgent {
memory_session_id: session.memorySessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
}, mode);
@@ -476,9 +468,11 @@ export class SDKAgent {
}
});
}
// Update Cursor context file for registered projects (fire-and-forget)
updateCursorContextForProject(session.project, getWorkerPort()).catch(() => {});
updateCursorContextForProject(session.project, getWorkerPort()).catch(error => {
logger.warn('CURSOR', 'Context update failed (non-critical)', { project: session.project }, error as Error);
});
}
// Mark messages as processed after successful observation/summary storage
@@ -549,7 +543,8 @@ export class SDKAgent {
if (claudePath) return claudePath;
} catch (error) {
logger.debug('SDK', 'Claude executable auto-detection failed', error);
// [ANTI-PATTERN IGNORED]: Fallback behavior - which/where failed, continue to throw clear error
logger.debug('SDK', 'Claude executable auto-detection failed', {}, error as Error);
}
throw new Error('Claude executable not found. Please either:\n1. Add "claude" to your system PATH, or\n2. Set CLAUDE_CODE_PATH in ~/.claude-mem/settings.json');
+5 -2
View File
@@ -1400,7 +1400,8 @@ export class SearchManager {
if (Array.isArray(filesRead) && filesRead.length > 0) {
lines.push(`**Files Read:** ${filesRead.join(', ')}`);
}
} catch {
} catch (error) {
logger.debug('WORKER', 'files_read is plain string, using as-is', {}, error as Error);
if (summary.files_read.trim()) {
lines.push(`**Files Read:** ${summary.files_read}`);
}
@@ -1414,7 +1415,8 @@ export class SearchManager {
if (Array.isArray(filesEdited) && filesEdited.length > 0) {
lines.push(`**Files Edited:** ${filesEdited.join(', ')}`);
}
} catch {
} catch (error) {
logger.debug('WORKER', 'files_edited is plain string, using as-is', {}, error as Error);
if (summary.files_edited.trim()) {
lines.push(`**Files Edited:** ${summary.files_edited}`);
}
@@ -1696,6 +1698,7 @@ export class SearchManager {
}]
};
} catch (error: any) {
logger.error('SEARCH', 'Timeline query failed', { query, anchor }, error);
return {
content: [{
type: 'text' as const,
+4 -3
View File
@@ -234,7 +234,7 @@ export class SessionManager {
* CRITICAL: Persists to database FIRST before adding to in-memory queue.
* This ensures summarize requests survive worker crashes.
*/
queueSummarize(sessionDbId: number, lastUserMessage: string, lastAssistantMessage?: string): void {
queueSummarize(sessionDbId: number, lastAssistantMessage?: string): void {
// Auto-initialize from database if needed (handles worker restarts)
let session = this.sessions.get(sessionDbId);
if (!session) {
@@ -244,7 +244,6 @@ export class SessionManager {
// CRITICAL: Persist to database FIRST
const message: PendingMessage = {
type: 'summarize',
last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage
};
@@ -286,7 +285,9 @@ export class SessionManager {
// Wait for generator to finish
if (session.generatorPromise) {
await session.generatorPromise.catch(() => {});
await session.generatorPromise.catch(error => {
logger.debug('SYSTEM', 'Generator already failed, cleaning up', { sessionId: session.sessionDbId });
});
}
// Cleanup
@@ -26,6 +26,7 @@ export abstract class BaseRouteHandler {
result.catch(error => this.handleError(res, error as Error));
}
} catch (error) {
logger.error('HTTP', 'Route handler error', { path: req.path }, error as Error);
this.handleError(res, error as Error);
}
};
+3 -2
View File
@@ -29,10 +29,11 @@ export function createMiddleware(
// HTTP request/response logging
middlewares.push((req: Request, res: Response, next: NextFunction) => {
// Skip logging for static assets and health checks
// Skip logging for static assets, health checks, and polling endpoints
const staticExtensions = ['.html', '.js', '.css', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.woff', '.woff2', '.ttf', '.eot'];
const isStaticAsset = staticExtensions.some(ext => req.path.endsWith(ext));
if (req.path.startsWith('/health') || req.path === '/' || isStaticAsset) {
const isPollingEndpoint = req.path === '/api/logs'; // Skip logs endpoint to avoid noise from auto-refresh
if (req.path.startsWith('/health') || req.path === '/' || isStaticAsset || isPollingEndpoint) {
return next();
}
@@ -55,6 +55,8 @@ export class DataRoutes extends BaseRouteHandler {
// Pending queue management endpoints
app.get('/api/pending-queue', this.handleGetPendingQueue.bind(this));
app.post('/api/pending-queue/process', this.handleProcessPendingQueue.bind(this));
app.delete('/api/pending-queue/failed', this.handleClearFailedQueue.bind(this));
app.delete('/api/pending-queue/all', this.handleClearAllQueue.bind(this));
// Import endpoint
app.post('/api/import', this.handleImport.bind(this));
@@ -423,4 +425,42 @@ export class DataRoutes extends BaseRouteHandler {
...result
});
});
/**
* Clear all failed messages from the queue
* DELETE /api/pending-queue/failed
* Returns the number of messages cleared
*/
private handleClearFailedQueue = this.wrapHandler((req: Request, res: Response): void => {
const { PendingMessageStore } = require('../../../sqlite/PendingMessageStore.js');
const pendingStore = new PendingMessageStore(this.dbManager.getSessionStore().db, 3);
const clearedCount = pendingStore.clearFailed();
logger.info('QUEUE', 'Cleared failed queue messages', { clearedCount });
res.json({
success: true,
clearedCount
});
});
/**
* Clear all messages from the queue (pending, processing, and failed)
* DELETE /api/pending-queue/all
* Returns the number of messages cleared
*/
private handleClearAllQueue = this.wrapHandler((req: Request, res: Response): void => {
const { PendingMessageStore } = require('../../../sqlite/PendingMessageStore.js');
const pendingStore = new PendingMessageStore(this.dbManager.getSessionStore().db, 3);
const clearedCount = pendingStore.clearAll();
logger.warn('QUEUE', 'Cleared ALL queue messages (pending, processing, failed)', { clearedCount });
res.json({
success: true,
clearedCount
});
});
}
@@ -0,0 +1,96 @@
/**
* Logs Routes
*
* Handles fetching and clearing log files from ~/.claude-mem/logs/
*/
import express, { Request, Response } from 'express';
import { readFileSync, existsSync, writeFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { logger } from '../../../../utils/logger.js';
import { SettingsDefaultsManager } from '../../../../shared/SettingsDefaultsManager.js';
import { BaseRouteHandler } from '../BaseRouteHandler.js';
export class LogsRoutes extends BaseRouteHandler {
private getLogFilePath(): string {
const dataDir = SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR');
const logsDir = join(dataDir, 'logs');
const date = new Date().toISOString().split('T')[0];
return join(logsDir, `claude-mem-${date}.log`);
}
private getLogsDir(): string {
const dataDir = SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR');
return join(dataDir, 'logs');
}
setupRoutes(app: express.Application): void {
app.get('/api/logs', this.handleGetLogs.bind(this));
app.post('/api/logs/clear', this.handleClearLogs.bind(this));
}
/**
* GET /api/logs
* Returns the current day's log file contents
* Query params:
* - lines: number of lines to return (default: 1000, max: 10000)
*/
private handleGetLogs = this.wrapHandler((req: Request, res: Response): void => {
const logFilePath = this.getLogFilePath();
if (!existsSync(logFilePath)) {
res.json({
logs: '',
path: logFilePath,
exists: false
});
return;
}
const requestedLines = parseInt(req.query.lines as string || '1000', 10);
const maxLines = Math.min(requestedLines, 10000); // Cap at 10k lines
const content = readFileSync(logFilePath, 'utf-8');
const lines = content.split('\n');
// Return the last N lines
const startIndex = Math.max(0, lines.length - maxLines);
const recentLines = lines.slice(startIndex).join('\n');
res.json({
logs: recentLines,
path: logFilePath,
exists: true,
totalLines: lines.length,
returnedLines: lines.length - startIndex
});
});
/**
* POST /api/logs/clear
* Clears the current day's log file
*/
private handleClearLogs = this.wrapHandler((req: Request, res: Response): void => {
const logFilePath = this.getLogFilePath();
if (!existsSync(logFilePath)) {
res.json({
success: true,
message: 'Log file does not exist',
path: logFilePath
});
return;
}
// Clear the log file by writing empty string
writeFileSync(logFilePath, '', 'utf-8');
logger.info('SYSTEM', 'Log file cleared via UI', { path: logFilePath });
res.json({
success: true,
message: 'Log file cleared',
path: logFilePath
});
});
}
@@ -211,6 +211,7 @@ export class SessionRoutes extends BaseRouteHandler {
}
} catch (e) {
// Ignore errors during recovery check, but still abort to prevent leaks
logger.debug('SESSION', 'Error during recovery check, aborting to prevent leaks', { sessionId: sessionDbId, error: e instanceof Error ? e.message : String(e) });
session.abortController.abort();
}
}
@@ -337,9 +338,9 @@ export class SessionRoutes extends BaseRouteHandler {
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
if (sessionDbId === null) return;
const { last_user_message, last_assistant_message } = req.body;
const { last_assistant_message } = req.body;
this.sessionManager.queueSummarize(sessionDbId, last_user_message, last_assistant_message);
this.sessionManager.queueSummarize(sessionDbId, last_assistant_message);
// CRITICAL: Ensure SDK agent is running to consume the queue
this.ensureGeneratorRunning(sessionDbId, 'summarize');
@@ -491,12 +492,12 @@ export class SessionRoutes extends BaseRouteHandler {
/**
* Queue summarize by contentSessionId (summary-hook uses this)
* POST /api/sessions/summarize
* Body: { contentSessionId, last_user_message, last_assistant_message }
* Body: { contentSessionId, last_assistant_message }
*
* Checks privacy, queues summarize request for SDK agent
*/
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { contentSessionId, last_user_message, last_assistant_message } = req.body;
const { contentSessionId, last_assistant_message } = req.body;
if (!contentSessionId) {
return this.badRequest(res, 'Missing contentSessionId');
@@ -522,17 +523,7 @@ export class SessionRoutes extends BaseRouteHandler {
}
// Queue summarize
this.sessionManager.queueSummarize(
sessionDbId,
last_user_message || logger.happyPathError(
'SESSION',
'Missing last_user_message when queueing summary in SessionRoutes',
{ sessionId: sessionDbId },
undefined,
''
),
last_assistant_message
);
this.sessionManager.queueSummarize(sessionDbId, last_assistant_message);
// Ensure SDK agent is running
this.ensureGeneratorRunning(sessionDbId, 'summarize');
@@ -348,7 +348,9 @@ export class SettingsRoutes extends BaseRouteHandler {
if (settings.CLAUDE_MEM_OPENROUTER_SITE_URL) {
try {
new URL(settings.CLAUDE_MEM_OPENROUTER_SITE_URL);
} catch {
} catch (error) {
// Invalid URL format
logger.debug('SETTINGS', 'Invalid URL format', { url: settings.CLAUDE_MEM_OPENROUTER_SITE_URL, error: error instanceof Error ? error.message : String(error) });
return { valid: false, error: 'CLAUDE_MEM_OPENROUTER_SITE_URL must be a valid URL' };
}
}
+5 -1
View File
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { SettingsDefaultsManager } from './SettingsDefaultsManager.js';
import { logger } from '../utils/logger.js';
// Get __dirname that works in both ESM (hooks) and CJS (worker) contexts
function getDirname(): string {
@@ -102,7 +103,10 @@ export function getCurrentProjectName(): string {
windowsHide: true
}).trim();
return basename(gitRoot);
} catch {
} catch (error) {
logger.debug('SYSTEM', 'Git root detection failed, using cwd basename', {
cwd: process.cwd()
}, error as Error);
return basename(process.cwd());
}
}
+4
View File
@@ -6,6 +6,7 @@
*/
import path from 'path';
import { logger } from '../utils/logger.js';
/**
* Parse JSON array string, returning empty array on failure
@@ -16,6 +17,9 @@ export function parseJsonArray(json: string | null): string[] {
const parsed = JSON.parse(json);
return Array.isArray(parsed) ? parsed : [];
} catch (err) {
logger.debug('PARSER', 'Failed to parse JSON array, using empty fallback', {
preview: json?.substring(0, 50)
}, err as Error);
return [];
}
}
+6 -2
View File
@@ -124,8 +124,12 @@ export async function ensureWorkerRunning(): Promise<void> {
await checkWorkerVersion(); // logs warning on mismatch, doesn't restart
return;
}
} catch {
// Continue polling
} catch (e) {
logger.debug('SYSTEM', 'Worker health check failed, will retry', {
attempt: i + 1,
maxRetries,
error: e instanceof Error ? e.message : String(e)
});
}
await new Promise(r => setTimeout(r, pollInterval));
}
+326
View File
@@ -2472,6 +2472,332 @@
border-color: var(--color-bg-button-hover);
}
/* Console Drawer - Chrome DevTools Style */
.console-toggle-btn {
position: fixed;
bottom: 20px;
left: 20px;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--color-bg-button);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
z-index: 999;
}
.console-toggle-btn:hover {
background: var(--color-bg-button-hover);
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.console-toggle-btn svg {
width: 20px;
height: 20px;
}
.console-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-bg-primary);
border-top: 1px solid var(--color-border-primary);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
flex-direction: column;
}
.console-resize-handle {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
cursor: ns-resize;
display: flex;
align-items: center;
justify-content: center;
}
.console-resize-handle:hover .console-resize-bar {
background: var(--color-bg-button);
}
.console-resize-bar {
width: 40px;
height: 3px;
border-radius: 2px;
background: var(--color-border-primary);
transition: background 0.2s ease;
}
.console-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border-primary);
background: var(--color-bg-header);
margin-top: 6px;
}
.console-tabs {
display: flex;
gap: 4px;
}
.console-tab {
padding: 4px 12px;
font-size: 12px;
color: var(--color-text-secondary);
background: transparent;
border: none;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.console-tab.active {
color: var(--color-text-primary);
border-bottom-color: var(--color-bg-button);
font-weight: 500;
}
.console-controls {
display: flex;
align-items: center;
gap: 8px;
}
.console-auto-refresh {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--color-text-secondary);
cursor: pointer;
user-select: none;
}
.console-auto-refresh input[type="checkbox"] {
cursor: pointer;
}
.console-control-btn {
background: transparent;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 4px 8px;
font-size: 14px;
border-radius: 4px;
transition: all 0.15s ease;
}
.console-control-btn:hover {
background: var(--color-bg-card-hover);
color: var(--color-text-primary);
}
.console-control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.console-clear-btn:hover {
color: var(--color-accent-error);
}
.console-content {
flex: 1;
overflow: auto;
background: var(--color-bg-primary);
}
.console-logs {
margin: 0;
padding: 8px 12px;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
color: var(--color-text-primary);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.console-error {
padding: 8px 12px;
background: rgba(239, 68, 68, 0.08);
border-bottom: 1px solid var(--color-accent-error);
color: var(--color-accent-error);
font-size: 11px;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
/* Console Filter Bar */
.console-filters {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 8px 12px;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-primary);
}
.console-filter-section {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.console-filter-label {
font-size: 10px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.console-filter-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.console-filter-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
font-size: 11px;
font-weight: 500;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.console-filter-chip:hover {
background: var(--color-bg-card-hover);
border-color: var(--chip-color, var(--color-border-hover));
color: var(--color-text-primary);
}
.console-filter-chip.active {
background: var(--chip-color, var(--color-accent-primary));
border-color: var(--chip-color, var(--color-accent-primary));
color: white;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
.console-filter-chip.active:hover {
opacity: 0.9;
}
.console-filter-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 12px;
background: transparent;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.15s ease;
}
.console-filter-action:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-hover);
color: var(--color-text-primary);
}
/* Log Line Styles */
.log-line {
display: block;
padding: 2px 0;
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-line-raw {
color: var(--color-text-secondary);
opacity: 0.8;
}
.log-line-empty {
color: var(--color-text-muted);
font-style: italic;
padding: 20px 0;
text-align: center;
}
.log-timestamp {
color: var(--color-text-muted);
opacity: 0.7;
}
.log-level {
font-weight: 500;
}
.log-component {
font-weight: 500;
}
.log-correlation {
color: var(--color-accent-primary);
opacity: 0.9;
}
.log-message {
color: inherit;
}
/* Log Level Colors in Dark Mode */
[data-theme="dark"] .log-line-raw {
color: #8b949e;
}
/* Responsive adjustments for filter bar */
@media (max-width: 600px) {
.console-filters {
flex-direction: column;
gap: 8px;
padding: 6px 10px;
}
.console-filter-section {
flex-wrap: wrap;
}
.console-filter-chip {
padding: 2px 6px;
font-size: 10px;
}
}
/* Responsive Modal */
@media (max-width: 900px) {
.modal-body {
+23
View File
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Header } from './components/Header';
import { Feed } from './components/Feed';
import { ContextSettingsModal } from './components/ContextSettingsModal';
import { LogsDrawer } from './components/LogsModal';
import { useSSE } from './hooks/useSSE';
import { useSettings } from './hooks/useSettings';
import { useStats } from './hooks/useStats';
@@ -13,6 +14,7 @@ import { mergeAndDeduplicateByProject } from './utils/data';
export function App() {
const [currentFilter, setCurrentFilter] = useState('');
const [contextPreviewOpen, setContextPreviewOpen] = useState(false);
const [logsModalOpen, setLogsModalOpen] = useState(false);
const [paginatedObservations, setPaginatedObservations] = useState<Observation[]>([]);
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
@@ -53,6 +55,11 @@ export function App() {
setContextPreviewOpen(prev => !prev);
}, []);
// Toggle logs modal
const toggleLogsModal = useCallback(() => {
setLogsModalOpen(prev => !prev);
}, []);
// Handle loading more data
const handleLoadMore = useCallback(async () => {
try {
@@ -116,6 +123,22 @@ export function App() {
isSaving={isSaving}
saveStatus={saveStatus}
/>
<button
className="console-toggle-btn"
onClick={toggleLogsModal}
title="Toggle Console"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</button>
<LogsDrawer
isOpen={logsModalOpen}
onClose={toggleLogsModal}
/>
</>
);
}
+458
View File
@@ -0,0 +1,458 @@
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
// Log levels and components matching the logger.ts definitions
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
type LogComponent = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' | 'HTTP' | 'SESSION' | 'CHROMA';
interface ParsedLogLine {
raw: string;
timestamp?: string;
level?: LogLevel;
component?: LogComponent;
correlationId?: string;
message?: string;
isSpecial?: 'dataIn' | 'dataOut' | 'success' | 'failure' | 'timing' | 'happyPath';
}
// Configuration for log levels
const LOG_LEVELS: { key: LogLevel; label: string; icon: string; color: string }[] = [
{ key: 'DEBUG', label: 'Debug', icon: '🔍', color: '#8b8b8b' },
{ key: 'INFO', label: 'Info', icon: '️', color: '#58a6ff' },
{ key: 'WARN', label: 'Warn', icon: '⚠️', color: '#d29922' },
{ key: 'ERROR', label: 'Error', icon: '❌', color: '#f85149' },
];
// Configuration for log components
const LOG_COMPONENTS: { key: LogComponent; label: string; icon: string; color: string }[] = [
{ key: 'HOOK', label: 'Hook', icon: '🪝', color: '#a371f7' },
{ key: 'WORKER', label: 'Worker', icon: '⚙️', color: '#58a6ff' },
{ key: 'SDK', label: 'SDK', icon: '📦', color: '#3fb950' },
{ key: 'PARSER', label: 'Parser', icon: '📄', color: '#79c0ff' },
{ key: 'DB', label: 'DB', icon: '🗄️', color: '#f0883e' },
{ key: 'SYSTEM', label: 'System', icon: '💻', color: '#8b949e' },
{ key: 'HTTP', label: 'HTTP', icon: '🌐', color: '#39d353' },
{ key: 'SESSION', label: 'Session', icon: '📋', color: '#db61a2' },
{ key: 'CHROMA', label: 'Chroma', icon: '🔮', color: '#a855f7' },
];
// Parse a single log line into structured data
function parseLogLine(line: string): ParsedLogLine {
// Pattern: [timestamp] [LEVEL] [COMPONENT] [correlation?] message
// Example: [2025-01-02 14:30:45.123] [INFO ] [WORKER] [session-123] → message
const pattern = /^\[([^\]]+)\]\s+\[(\w+)\s*\]\s+\[(\w+)\s*\]\s+(?:\[([^\]]+)\]\s+)?(.*)$/;
const match = line.match(pattern);
if (!match) {
return { raw: line };
}
const [, timestamp, level, component, correlationId, message] = match;
// Detect special message types
let isSpecial: ParsedLogLine['isSpecial'] = undefined;
if (message.startsWith('→')) isSpecial = 'dataIn';
else if (message.startsWith('←')) isSpecial = 'dataOut';
else if (message.startsWith('✓')) isSpecial = 'success';
else if (message.startsWith('✗')) isSpecial = 'failure';
else if (message.startsWith('⏱')) isSpecial = 'timing';
else if (message.includes('[HAPPY-PATH]')) isSpecial = 'happyPath';
return {
raw: line,
timestamp,
level: level?.trim() as LogLevel,
component: component?.trim() as LogComponent,
correlationId: correlationId || undefined,
message,
isSpecial,
};
}
interface LogsDrawerProps {
isOpen: boolean;
onClose: () => void;
}
export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
const [logs, setLogs] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [autoRefresh, setAutoRefresh] = useState(false);
const [height, setHeight] = useState(350);
const [isResizing, setIsResizing] = useState(false);
const startYRef = useRef(0);
const startHeightRef = useRef(0);
const contentRef = useRef<HTMLDivElement>(null);
const wasAtBottomRef = useRef(true);
// Filter state
const [activeLevels, setActiveLevels] = useState<Set<LogLevel>>(
new Set(['DEBUG', 'INFO', 'WARN', 'ERROR'])
);
const [activeComponents, setActiveComponents] = useState<Set<LogComponent>>(
new Set(['HOOK', 'WORKER', 'SDK', 'PARSER', 'DB', 'SYSTEM', 'HTTP', 'SESSION', 'CHROMA'])
);
// Parse and filter log lines
const parsedLines = useMemo(() => {
if (!logs) return [];
return logs.split('\n').map(parseLogLine);
}, [logs]);
const filteredLines = useMemo(() => {
return parsedLines.filter(line => {
// Always show unparsed lines
if (!line.level || !line.component) return true;
return activeLevels.has(line.level) && activeComponents.has(line.component);
});
}, [parsedLines, activeLevels, activeComponents]);
// Check if user is at bottom before updating
const checkIfAtBottom = useCallback(() => {
if (!contentRef.current) return true;
const { scrollTop, scrollHeight, clientHeight } = contentRef.current;
return scrollHeight - scrollTop - clientHeight < 50;
}, []);
// Auto-scroll to bottom
const scrollToBottom = useCallback(() => {
if (contentRef.current && wasAtBottomRef.current) {
contentRef.current.scrollTop = contentRef.current.scrollHeight;
}
}, []);
const fetchLogs = useCallback(async () => {
// Save scroll position before fetch
wasAtBottomRef.current = checkIfAtBottom();
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/logs');
if (!response.ok) {
throw new Error(`Failed to fetch logs: ${response.statusText}`);
}
const data = await response.json();
setLogs(data.logs || '');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}, [checkIfAtBottom]);
// Scroll to bottom after logs update
useEffect(() => {
scrollToBottom();
}, [logs, scrollToBottom]);
const handleClearLogs = useCallback(async () => {
if (!confirm('Are you sure you want to clear all logs?')) {
return;
}
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/logs/clear', { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to clear logs: ${response.statusText}`);
}
setLogs('');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}, []);
// Handle resize
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
startYRef.current = e.clientY;
startHeightRef.current = height;
}, [height]);
useEffect(() => {
if (!isResizing) return;
const handleMouseMove = (e: MouseEvent) => {
const deltaY = startYRef.current - e.clientY;
const newHeight = Math.min(Math.max(150, startHeightRef.current + deltaY), window.innerHeight - 100);
setHeight(newHeight);
};
const handleMouseUp = () => {
setIsResizing(false);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizing]);
// Fetch logs when drawer opens
useEffect(() => {
if (isOpen) {
wasAtBottomRef.current = true; // Start at bottom on open
fetchLogs();
}
}, [isOpen, fetchLogs]);
// Auto-refresh logs every 2 seconds if enabled
useEffect(() => {
if (!isOpen || !autoRefresh) {
return;
}
const interval = setInterval(fetchLogs, 2000);
return () => clearInterval(interval);
}, [isOpen, autoRefresh, fetchLogs]);
// Toggle level filter
const toggleLevel = useCallback((level: LogLevel) => {
setActiveLevels(prev => {
const next = new Set(prev);
if (next.has(level)) {
next.delete(level);
} else {
next.add(level);
}
return next;
});
}, []);
// Toggle component filter
const toggleComponent = useCallback((component: LogComponent) => {
setActiveComponents(prev => {
const next = new Set(prev);
if (next.has(component)) {
next.delete(component);
} else {
next.add(component);
}
return next;
});
}, []);
// Select all / none for levels
const setAllLevels = useCallback((enabled: boolean) => {
if (enabled) {
setActiveLevels(new Set(['DEBUG', 'INFO', 'WARN', 'ERROR']));
} else {
setActiveLevels(new Set());
}
}, []);
// Select all / none for components
const setAllComponents = useCallback((enabled: boolean) => {
if (enabled) {
setActiveComponents(new Set(['HOOK', 'WORKER', 'SDK', 'PARSER', 'DB', 'SYSTEM', 'HTTP', 'SESSION', 'CHROMA']));
} else {
setActiveComponents(new Set());
}
}, []);
if (!isOpen) {
return null;
}
// Get style for a parsed log line
const getLineStyle = (line: ParsedLogLine): React.CSSProperties => {
const levelConfig = LOG_LEVELS.find(l => l.key === line.level);
const componentConfig = LOG_COMPONENTS.find(c => c.key === line.component);
let color = 'var(--color-text-primary)';
let fontWeight = 'normal';
let backgroundColor = 'transparent';
if (line.level === 'ERROR') {
color = '#f85149';
backgroundColor = 'rgba(248, 81, 73, 0.1)';
} else if (line.level === 'WARN') {
color = '#d29922';
backgroundColor = 'rgba(210, 153, 34, 0.05)';
} else if (line.isSpecial === 'success') {
color = '#3fb950';
} else if (line.isSpecial === 'failure') {
color = '#f85149';
} else if (line.isSpecial === 'happyPath') {
color = '#d29922';
} else if (levelConfig) {
color = levelConfig.color;
}
return { color, fontWeight, backgroundColor, padding: '1px 0', borderRadius: '2px' };
};
// Render a single log line with syntax highlighting
const renderLogLine = (line: ParsedLogLine, index: number) => {
if (!line.timestamp) {
// Unparsed line - render as-is
return (
<div key={index} className="log-line log-line-raw">
{line.raw}
</div>
);
}
const levelConfig = LOG_LEVELS.find(l => l.key === line.level);
const componentConfig = LOG_COMPONENTS.find(c => c.key === line.component);
return (
<div key={index} className="log-line" style={getLineStyle(line)}>
<span className="log-timestamp">[{line.timestamp}]</span>
{' '}
<span className="log-level" style={{ color: levelConfig?.color }} title={line.level}>
[{levelConfig?.icon || ''} {line.level?.padEnd(5)}]
</span>
{' '}
<span className="log-component" style={{ color: componentConfig?.color }} title={line.component}>
[{componentConfig?.icon || ''} {line.component?.padEnd(7)}]
</span>
{' '}
{line.correlationId && (
<>
<span className="log-correlation">[{line.correlationId}]</span>
{' '}
</>
)}
<span className="log-message">{line.message}</span>
</div>
);
};
return (
<div className="console-drawer" style={{ height: `${height}px` }}>
<div
className="console-resize-handle"
onMouseDown={handleMouseDown}
>
<div className="console-resize-bar" />
</div>
<div className="console-header">
<div className="console-tabs">
<div className="console-tab active">Console</div>
</div>
<div className="console-controls">
<label className="console-auto-refresh">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
/>
Auto-refresh
</label>
<button
className="console-control-btn"
onClick={fetchLogs}
disabled={isLoading}
title="Refresh logs"
>
</button>
<button
className="console-control-btn"
onClick={() => {
wasAtBottomRef.current = true;
scrollToBottom();
}}
title="Scroll to bottom"
>
</button>
<button
className="console-control-btn console-clear-btn"
onClick={handleClearLogs}
disabled={isLoading}
title="Clear logs"
>
🗑
</button>
<button
className="console-control-btn"
onClick={onClose}
title="Close console"
>
</button>
</div>
</div>
{/* Filter Bar */}
<div className="console-filters">
<div className="console-filter-section">
<span className="console-filter-label">Levels:</span>
<div className="console-filter-chips">
{LOG_LEVELS.map(level => (
<button
key={level.key}
className={`console-filter-chip ${activeLevels.has(level.key) ? 'active' : ''}`}
onClick={() => toggleLevel(level.key)}
style={{
'--chip-color': level.color,
} as React.CSSProperties}
title={level.label}
>
{level.icon} {level.label}
</button>
))}
<button
className="console-filter-action"
onClick={() => setAllLevels(activeLevels.size === 0)}
title={activeLevels.size === LOG_LEVELS.length ? 'Select none' : 'Select all'}
>
{activeLevels.size === LOG_LEVELS.length ? '○' : '●'}
</button>
</div>
</div>
<div className="console-filter-section">
<span className="console-filter-label">Components:</span>
<div className="console-filter-chips">
{LOG_COMPONENTS.map(comp => (
<button
key={comp.key}
className={`console-filter-chip ${activeComponents.has(comp.key) ? 'active' : ''}`}
onClick={() => toggleComponent(comp.key)}
style={{
'--chip-color': comp.color,
} as React.CSSProperties}
title={comp.label}
>
{comp.icon} {comp.label}
</button>
))}
<button
className="console-filter-action"
onClick={() => setAllComponents(activeComponents.size === 0)}
title={activeComponents.size === LOG_COMPONENTS.length ? 'Select none' : 'Select all'}
>
{activeComponents.size === LOG_COMPONENTS.length ? '○' : '●'}
</button>
</div>
</div>
</div>
{error && (
<div className="console-error">
{error}
</div>
)}
<div className="console-content" ref={contentRef}>
<div className="console-logs">
{filteredLines.length === 0 ? (
<div className="log-line log-line-empty">No logs available</div>
) : (
filteredLines.map((line, index) => renderLogLine(line, index))
)}
</div>
</div>
</div>
);
}
+1
View File
@@ -58,6 +58,7 @@ export function useContextPreview(settings: Settings): UseContextPreviewResult {
setError('Failed to load preview');
}
} catch (err) {
console.warn('Failed to load context preview:', err);
setError((err as Error).message);
} finally {
setIsLoading(false);
+1
View File
@@ -78,6 +78,7 @@ export function useSettings() {
setSaveStatus(`✗ Error: ${result.error}`);
}
} catch (error) {
console.error('Failed to save settings:', error);
setSaveStatus(`✗ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSaving(false);
+5 -2
View File
@@ -9,6 +9,7 @@ import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { logger } from './logger.js';
/**
* Get the Bun executable path
@@ -28,8 +29,10 @@ export function getBunPath(): string | null {
if (result.status === 0) {
return 'bun'; // Available in PATH
}
} catch {
// Not in PATH, continue to check common locations
} catch (e) {
logger.debug('SYSTEM', 'Bun not found in PATH, checking common installation locations', {
error: e instanceof Error ? e.message : String(e)
});
}
// Check common installation paths
+16 -5
View File
@@ -7,6 +7,7 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
import { join, basename } from 'path';
import { logger } from './logger.js';
// ============================================================================
// Types
@@ -40,7 +41,11 @@ export function readCursorRegistry(registryFile: string): CursorProjectRegistry
try {
if (!existsSync(registryFile)) return {};
return JSON.parse(readFileSync(registryFile, 'utf-8'));
} catch {
} catch (error) {
logger.warn('CONFIG', 'Failed to read Cursor registry, using empty registry', {
file: registryFile,
error: error instanceof Error ? error.message : String(error)
});
return {};
}
}
@@ -145,8 +150,11 @@ export function configureCursorMcp(mcpJsonPath: string, mcpServerScriptPath: str
if (!config.mcpServers) {
config.mcpServers = {};
}
} catch {
// Start fresh if corrupt
} catch (error) {
logger.warn('CONFIG', 'Failed to read MCP config, starting fresh', {
file: mcpJsonPath,
error: error instanceof Error ? error.message : String(error)
});
config = { mcpServers: {} };
}
}
@@ -173,8 +181,11 @@ export function removeMcpConfig(mcpJsonPath: string): void {
delete config.mcpServers['claude-mem'];
writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2));
}
} catch {
// Ignore errors during cleanup
} catch (e) {
logger.warn('CURSOR', 'Failed to remove MCP config during cleanup', {
mcpJsonPath,
error: e instanceof Error ? e.message : String(e)
});
}
}
+2 -1
View File
@@ -267,7 +267,8 @@ class Logger {
try {
appendFileSync(this.logFilePath, logLine + '\n', 'utf8');
} catch (error) {
// If file write fails, write to stderr as last resort
// Logger can't log its own failures - use stderr as last resort
// This is expected during disk full / permission errors
process.stderr.write(`[LOGGER] Failed to write to log file: ${error}\n`);
}
} else {
+11
View File
@@ -4,6 +4,7 @@
*/
import { readFileSync } from 'fs';
import { logger } from './logger.js';
import type {
TranscriptEntry,
UserTranscriptEntry,
@@ -42,12 +43,22 @@ export class TranscriptParser {
const entry = JSON.parse(line) as TranscriptEntry;
this.entries.push(entry);
} catch (error) {
logger.debug('PARSER', 'Failed to parse transcript line', { lineNumber: index + 1 }, error as Error);
this.parseErrors.push({
lineNumber: index + 1,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Log summary if there were parse errors
if (this.parseErrors.length > 0) {
logger.warn('PARSER', `Failed to parse ${this.parseErrors.length} lines`, {
path: transcriptPath,
totalLines: lines.length,
errorCount: this.parseErrors.length
});
}
}
/**
+502
View File
@@ -0,0 +1,502 @@
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { SessionStore } from '../src/services/sqlite/SessionStore.js';
/**
* Session ID Usage Validation Tests
*
* PURPOSE: Prevent confusion and bugs from mixing contentSessionId and memorySessionId
*
* CRITICAL ARCHITECTURE:
* - contentSessionId: User's Claude Code conversation session (immutable)
* - memorySessionId: SDK agent's session ID for resume (captured from SDK response)
*
* INVARIANTS TO ENFORCE:
* 1. memorySessionId starts equal to contentSessionId (placeholder for FK)
* 2. Resume MUST NOT be used when memorySessionId === contentSessionId
* 3. Resume MUST ONLY be used when hasRealMemorySessionId === true
* 4. Observations are stored with contentSessionId (not the captured SDK memorySessionId)
* 5. updateMemorySessionId() is required before resume can work
*/
describe('Session ID Usage Validation', () => {
let store: SessionStore;
beforeEach(() => {
store = new SessionStore(':memory:');
});
afterEach(() => {
store.close();
});
describe('Placeholder Detection - hasRealMemorySessionId Logic', () => {
it('should identify placeholder when memorySessionId equals contentSessionId', () => {
const contentSessionId = 'user-session-123';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test prompt');
const session = store.getSessionById(sessionDbId);
// Initially, they're equal (placeholder state)
expect(session?.memory_session_id).toBe(session?.content_session_id);
// hasRealMemorySessionId would be FALSE
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
expect(hasRealMemorySessionId).toBe(false);
});
it('should identify real memory session ID after capture', () => {
const contentSessionId = 'user-session-456';
const capturedMemoryId = 'sdk-generated-abc123';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test prompt');
store.updateMemorySessionId(sessionDbId, capturedMemoryId);
const session = store.getSessionById(sessionDbId);
// After capture, they're different (real memory session ID)
expect(session?.memory_session_id).not.toBe(session?.content_session_id);
// hasRealMemorySessionId would be TRUE
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
expect(hasRealMemorySessionId).toBe(true);
});
it('should never use contentSessionId as resume parameter when in placeholder state', () => {
const contentSessionId = 'dangerous-session-789';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
const session = store.getSessionById(sessionDbId);
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
// CRITICAL: This check prevents resuming the USER'S session instead of memory session
if (hasRealMemorySessionId) {
// Safe to use for resume
const resumeParam = session?.memory_session_id;
expect(resumeParam).not.toBe(contentSessionId);
} else {
// Must NOT pass resume parameter
// Resume should be undefined/null in SDK call
expect(hasRealMemorySessionId).toBe(false);
}
});
});
describe('Observation Storage - ContentSessionId Usage', () => {
it('should store observations with contentSessionId in memory_session_id column', () => {
const contentSessionId = 'obs-content-session-123';
store.createSDKSession(contentSessionId, 'test-project', 'Test');
const obs = {
type: 'discovery',
title: 'Test Observation',
subtitle: null,
facts: ['Fact 1'],
narrative: 'Testing',
concepts: ['testing'],
files_read: [],
files_modified: []
};
// SDKAgent.ts line 332 passes session.contentSessionId here
const result = store.storeObservation(contentSessionId, 'test-project', obs, 1);
// Verify it's stored in the memory_session_id column with contentSessionId value
const stored = store.db.prepare(
'SELECT memory_session_id FROM observations WHERE id = ?'
).get(result.id) as { memory_session_id: string };
// CRITICAL: memory_session_id column contains contentSessionId, not the captured SDK session ID
expect(stored.memory_session_id).toBe(contentSessionId);
});
it('should be retrievable using contentSessionId (observations use contentSessionId)', () => {
const contentSessionId = 'retrieval-test-session';
store.createSDKSession(contentSessionId, 'test-project', 'Test');
// Store observation with contentSessionId
const obs = {
type: 'feature',
title: 'Observation',
subtitle: null,
facts: [],
narrative: null,
concepts: [],
files_read: [],
files_modified: []
};
store.storeObservation(contentSessionId, 'test-project', obs, 1);
// Observations are retrievable by contentSessionId
// (because storeObservation receives contentSessionId and stores it in memory_session_id column)
const observations = store.getObservationsForSession(contentSessionId);
expect(observations.length).toBe(1);
expect(observations[0].title).toBe('Observation');
});
});
describe('Resume Safety - Prevent contentSessionId Resume Bug', () => {
it('should prevent resume with placeholder memorySessionId', () => {
const contentSessionId = 'safety-test-session';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
const session = store.getSessionById(sessionDbId);
// Simulate hasRealMemorySessionId check from SDKAgent.ts line 75-76
const hasRealMemorySessionId = session?.memory_session_id &&
session.memory_session_id !== session.content_session_id;
// MUST be false in placeholder state
expect(hasRealMemorySessionId).toBe(false);
// Resume parameter should NOT be set
// In SDK call: ...(hasRealMemorySessionId && { resume: session.memorySessionId })
// This evaluates to an empty object, not a resume parameter
const resumeOptions = hasRealMemorySessionId ? { resume: session?.memory_session_id } : {};
expect(resumeOptions).toEqual({});
});
it('should allow resume only after memory session ID is captured', () => {
const contentSessionId = 'resume-ready-session';
const capturedMemoryId = 'real-sdk-session-123';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
// Before capture - no resume
let session = store.getSessionById(sessionDbId);
let hasRealMemorySessionId = session?.memory_session_id &&
session.memory_session_id !== session.content_session_id;
expect(hasRealMemorySessionId).toBe(false);
// Capture memory session ID
store.updateMemorySessionId(sessionDbId, capturedMemoryId);
// After capture - resume allowed
session = store.getSessionById(sessionDbId);
hasRealMemorySessionId = session?.memory_session_id &&
session.memory_session_id !== session.content_session_id;
expect(hasRealMemorySessionId).toBe(true);
// Resume parameter should be the captured ID
const resumeOptions = hasRealMemorySessionId ? { resume: session?.memory_session_id } : {};
expect(resumeOptions).toEqual({ resume: capturedMemoryId });
expect(resumeOptions.resume).not.toBe(contentSessionId);
});
});
describe('Cross-Contamination Prevention', () => {
it('should never mix observations from different content sessions', () => {
const session1 = 'user-session-A';
const session2 = 'user-session-B';
store.createSDKSession(session1, 'project-a', 'Prompt A');
store.createSDKSession(session2, 'project-b', 'Prompt B');
// Store observations in each session
store.storeObservation(session1, 'project-a', {
type: 'discovery',
title: 'Observation A',
subtitle: null,
facts: [],
narrative: null,
concepts: [],
files_read: [],
files_modified: []
}, 1);
store.storeObservation(session2, 'project-b', {
type: 'discovery',
title: 'Observation B',
subtitle: null,
facts: [],
narrative: null,
concepts: [],
files_read: [],
files_modified: []
}, 1);
// Verify isolation
const obsA = store.getObservationsForSession(session1);
const obsB = store.getObservationsForSession(session2);
expect(obsA.length).toBe(1);
expect(obsB.length).toBe(1);
expect(obsA[0].title).toBe('Observation A');
expect(obsB[0].title).toBe('Observation B');
});
it('should never leak memory session IDs between content sessions', () => {
const content1 = 'content-session-1';
const content2 = 'content-session-2';
const memory1 = 'memory-session-1';
const memory2 = 'memory-session-2';
const id1 = store.createSDKSession(content1, 'project', 'Prompt');
const id2 = store.createSDKSession(content2, 'project', 'Prompt');
store.updateMemorySessionId(id1, memory1);
store.updateMemorySessionId(id2, memory2);
const session1 = store.getSessionById(id1);
const session2 = store.getSessionById(id2);
// Each session must have its own unique memory session ID
expect(session1?.memory_session_id).toBe(memory1);
expect(session2?.memory_session_id).toBe(memory2);
expect(session1?.memory_session_id).not.toBe(session2?.memory_session_id);
});
});
describe('Foreign Key Integrity', () => {
it('should cascade delete observations when session is deleted', () => {
const contentSessionId = 'cascade-test-session';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
// Store observation
const obs = {
type: 'discovery',
title: 'Will be deleted',
subtitle: null,
facts: [],
narrative: null,
concepts: [],
files_read: [],
files_modified: []
};
store.storeObservation(contentSessionId, 'test-project', obs, 1);
// Verify observation exists
let observations = store.getObservationsForSession(contentSessionId);
expect(observations.length).toBe(1);
// Delete session (should cascade to observations)
store.db.prepare('DELETE FROM sdk_sessions WHERE id = ?').run(sessionDbId);
// Verify observations were deleted
observations = store.getObservationsForSession(contentSessionId);
expect(observations.length).toBe(0);
});
it('should maintain FK relationship between observations and sessions', () => {
const contentSessionId = 'fk-test-session';
store.createSDKSession(contentSessionId, 'test-project', 'Test');
// This should succeed (FK exists)
expect(() => {
store.storeObservation(contentSessionId, 'test-project', {
type: 'discovery',
title: 'Valid FK',
subtitle: null,
facts: [],
narrative: null,
concepts: [],
files_read: [],
files_modified: []
}, 1);
}).not.toThrow();
// This should fail (FK doesn't exist)
expect(() => {
store.storeObservation('nonexistent-session-id', 'test-project', {
type: 'discovery',
title: 'Invalid FK',
subtitle: null,
facts: [],
narrative: null,
concepts: [],
files_read: [],
files_modified: []
}, 1);
}).toThrow();
});
});
describe('Session Lifecycle - Memory ID Capture Flow', () => {
it('should follow correct lifecycle: create → capture → resume', () => {
const contentSessionId = 'lifecycle-session';
// STEP 1: Hook creates session (memory_session_id = content_session_id)
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'First prompt');
let session = store.getSessionById(sessionDbId);
expect(session?.memory_session_id).toBe(contentSessionId); // Placeholder
// STEP 2: First SDK message arrives with real session ID
const realMemoryId = 'sdk-generated-session-xyz';
store.updateMemorySessionId(sessionDbId, realMemoryId);
session = store.getSessionById(sessionDbId);
expect(session?.memory_session_id).toBe(realMemoryId); // Real ID
// STEP 3: Subsequent prompts can now resume
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
expect(hasRealMemorySessionId).toBe(true);
// Resume parameter is safe to use
const resumeParam = session?.memory_session_id;
expect(resumeParam).toBe(realMemoryId);
expect(resumeParam).not.toBe(contentSessionId);
});
it('should handle worker restart by preserving captured memory session ID', () => {
const contentSessionId = 'restart-test-session';
const capturedMemoryId = 'persisted-memory-id';
// Simulate first worker instance
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Prompt');
store.updateMemorySessionId(sessionDbId, capturedMemoryId);
// Simulate worker restart - session re-fetched from database
const session = store.getSessionById(sessionDbId);
// Memory session ID should be preserved
expect(session?.memory_session_id).toBe(capturedMemoryId);
// Resume can work immediately
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
expect(hasRealMemorySessionId).toBe(true);
});
});
describe('CRITICAL: 1:1 Transcript Mapping Guarantees', () => {
it('should enforce UNIQUE constraint on memory_session_id (prevents duplicate memory transcripts)', () => {
const content1 = 'content-session-1';
const content2 = 'content-session-2';
const sharedMemoryId = 'shared-memory-id';
const id1 = store.createSDKSession(content1, 'project', 'Prompt 1');
const id2 = store.createSDKSession(content2, 'project', 'Prompt 2');
// First session captures memory ID - should succeed
store.updateMemorySessionId(id1, sharedMemoryId);
// Second session tries to use SAME memory ID - should FAIL
expect(() => {
store.updateMemorySessionId(id2, sharedMemoryId);
}).toThrow(); // UNIQUE constraint violation
// Verify first session still has the ID
const session1 = store.getSessionById(id1);
expect(session1?.memory_session_id).toBe(sharedMemoryId);
});
it('should prevent memorySessionId from being changed after real capture (single transition guarantee)', () => {
const contentSessionId = 'single-capture-test';
const firstMemoryId = 'first-sdk-session-id';
const secondMemoryId = 'different-sdk-session-id';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
// First capture - should succeed
store.updateMemorySessionId(sessionDbId, firstMemoryId);
let session = store.getSessionById(sessionDbId);
expect(session?.memory_session_id).toBe(firstMemoryId);
// Second capture with DIFFERENT ID - should FAIL (or be no-op in proper implementation)
// This test documents current behavior - ideally updateMemorySessionId should
// check if memorySessionId already differs from contentSessionId and refuse to update
store.updateMemorySessionId(sessionDbId, secondMemoryId);
session = store.getSessionById(sessionDbId);
// CRITICAL: If this allows the update, we could get multiple memory transcripts!
// This test currently shows the vulnerability - in production, SDKAgent.ts
// has the check `if (!session.memorySessionId)` which should prevent this,
// but the database layer doesn't enforce it.
//
// For now, we document that the second update DOES go through (current behavior)
expect(session?.memory_session_id).toBe(secondMemoryId);
// TODO: Add database-level protection via CHECK constraint or trigger
// to prevent changing memory_session_id once it differs from content_session_id
});
it('should use same memorySessionId for all prompts in a conversation (resume consistency)', () => {
const contentSessionId = 'multi-prompt-session';
const realMemoryId = 'consistent-memory-id';
// Prompt 1: Create session
let sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Prompt 1');
let session = store.getSessionById(sessionDbId);
// Initially placeholder
expect(session?.memory_session_id).toBe(contentSessionId);
// Prompt 1: Capture real memory ID
store.updateMemorySessionId(sessionDbId, realMemoryId);
// Prompt 2: Look up session by contentSessionId (simulates hook creating session again)
sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Prompt 2');
session = store.getSessionById(sessionDbId);
// Should get SAME memory ID (resume with this)
expect(session?.memory_session_id).toBe(realMemoryId);
// Prompt 3: Again, same contentSessionId
sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Prompt 3');
session = store.getSessionById(sessionDbId);
// Should STILL get same memory ID
expect(session?.memory_session_id).toBe(realMemoryId);
// All three prompts use the SAME memorySessionId → ONE memory transcript file
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
expect(hasRealMemorySessionId).toBe(true);
});
it('should lookup session by contentSessionId and retrieve memorySessionId for resume', () => {
const contentSessionId = 'lookup-test-session';
const capturedMemoryId = 'memory-for-resume';
// First prompt: Create and capture
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'First');
store.updateMemorySessionId(sessionDbId, capturedMemoryId);
// Second prompt: Hook provides contentSessionId, needs to lookup memorySessionId
// The createSDKSession method IS the lookup (INSERT OR IGNORE + SELECT)
const lookedUpSessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Second');
// Should be same DB row
expect(lookedUpSessionDbId).toBe(sessionDbId);
// Get session to extract memorySessionId for resume
const session = store.getSessionById(lookedUpSessionDbId);
const resumeParam = session?.memory_session_id;
// This is what would be passed to SDK query({ resume: resumeParam })
expect(resumeParam).toBe(capturedMemoryId);
expect(resumeParam).not.toBe(contentSessionId);
});
});
describe('Edge Cases - Session ID Equality', () => {
it('should handle case where SDK returns session ID equal to contentSessionId', () => {
// Edge case: SDK happens to generate same ID as content session
const contentSessionId = 'same-id-123';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
// SDK returns the same ID (unlikely but possible)
store.updateMemorySessionId(sessionDbId, contentSessionId);
const session = store.getSessionById(sessionDbId);
const hasRealMemorySessionId = session?.memory_session_id !== session?.content_session_id;
// Would be FALSE, so resume would not be used
// This is safe - worst case is a fresh session instead of resume
expect(hasRealMemorySessionId).toBe(false);
});
it('should handle NULL memory_session_id gracefully', () => {
const contentSessionId = 'null-test-session';
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
// Manually set memory_session_id to NULL (shouldn't happen in practice)
store.db.prepare('UPDATE sdk_sessions SET memory_session_id = NULL WHERE id = ?').run(sessionDbId);
const session = store.getSessionById(sessionDbId);
const hasRealMemorySessionId = session?.memory_session_id &&
session.memory_session_id !== session.content_session_id;
// Should be falsy (NULL is falsy)
expect(hasRealMemorySessionId).toBeFalsy();
});
});
});