Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5e266fc80 | |||
| 417acb0f81 | |||
| c2fbb39fd0 | |||
| 564249f533 | |||
| f5b0fbc7cb | |||
| ff86e21697 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "8.5.2",
|
||||
"version": "8.5.3",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
+185
-285
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,143 @@
|
||||
/* To @claude: be vigilant about only leaving evergreen context in this file, claude-mem handles working context separately. */
|
||||
|
||||
# ⚠️ MANDATORY ERROR HANDLING RULES ⚠️
|
||||
|
||||
## 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/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: Approved override for justified exceptions
|
||||
try {
|
||||
JSON.parse(optionalField);
|
||||
} catch (error) {
|
||||
// [APPROVED OVERRIDE]: Expected JSON parse failures for optional fields, too frequent to log
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
### Approved Overrides
|
||||
|
||||
When you have a **justified reason** to violate the error handling rules (e.g., performance-critical hot paths, expected frequent failures), you can use an approved override:
|
||||
|
||||
```typescript
|
||||
// [APPROVED OVERRIDE]: Brief explanation of why this is necessary
|
||||
```
|
||||
|
||||
**Rules for approved overrides:**
|
||||
- Must have a **specific, technical reason** (not "seemed fine" or "works for me")
|
||||
- Reason must explain **why the violation is necessary**, not just what it does
|
||||
- Examples of valid reasons:
|
||||
- "Expected JSON parse failures for optional fields, too frequent to log"
|
||||
- "Logger can't log its own failures, using stderr as last resort"
|
||||
- "Health check port scan, expected connection failures"
|
||||
- The detector will flag these as **APPROVED_OVERRIDE** (warning level) for review
|
||||
- Invalid or outdated reasons should be challenged during code review
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
# Claude-Mem: AI Development Instructions
|
||||
|
||||
## What This Project Is
|
||||
|
||||
@@ -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`
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "8.5.2",
|
||||
"version": "8.5.3",
|
||||
"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,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "8.5.2",
|
||||
"version": "8.5.3",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)});
|
||||
|
||||
@@ -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)});
|
||||
|
||||
@@ -3,8 +3,8 @@ import{stdin as $}from"process";var f=JSON.stringify({continue:!0,suppressOutput
|
||||
${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}
|
||||
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let O="";if(n){let{sessionId:R,memorySessionId:Z,correlationId:tt,...U}=n;Object.keys(U).length>0&&(O=` {${Object.entries(U).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let D=`[${E}] [${i}] [${_}] ${a}${e}${O}${l}`;if(this.logFilePath)try{W(this.logFilePath,D+`
|
||||
`,"utf8")}catch(R){process.stderr.write(`[LOGGER] Failed to write to log file: ${R}
|
||||
`)}else process.stderr.write(D+`
|
||||
`)}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}
|
||||
@@ -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 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=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(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(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(`
|
||||
`),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,`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+304
-255
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2472,6 +2472,177 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/* Responsive Modal */
|
||||
@media (max-width: 900px) {
|
||||
.modal-body {
|
||||
|
||||
Executable
+256
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,435 @@
|
||||
#!/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));
|
||||
|
||||
// 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)/) ||
|
||||
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 [APPROVED OVERRIDE] marker
|
||||
const overrideMatch = catchContent.match(/\/\/\s*\[APPROVED OVERRIDE\]:\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)/);
|
||||
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 continuesExecution = !hasReturn; // If no return/throw, execution continues
|
||||
|
||||
if (continuesExecution && hasLogging) {
|
||||
if (overrideReason) {
|
||||
antiPatterns.push({
|
||||
file: relPath,
|
||||
line: catchStartLine,
|
||||
pattern: 'CATCH_AND_CONTINUE_CRITICAL_PATH',
|
||||
severity: 'APPROVED_OVERRIDE',
|
||||
description: 'Critical path continues after error - approved override.',
|
||||
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 approve an anti-pattern, add: // [APPROVED OVERRIDE]: 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);
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -96,13 +96,17 @@ export function buildObservationPrompt(obs: Observation): string {
|
||||
|
||||
try {
|
||||
toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Expected: tool_input may not be valid JSON (e.g., plain strings)
|
||||
// Not logging - this is a normal fallback for non-JSON tool inputs
|
||||
toolInput = obs.tool_input; // If parse fails, use raw value
|
||||
}
|
||||
|
||||
try {
|
||||
toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Expected: tool_output may not be valid JSON (e.g., plain strings)
|
||||
// Not logging - this is a normal fallback for non-JSON tool outputs
|
||||
toolOutput = obs.tool_output; // If parse fails, use raw value
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -200,6 +200,8 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Expected: malformed JSON lines in transcript
|
||||
// Not logging - this loops through many lines, logging each would be excessive
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -227,7 +229,8 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
|
||||
try {
|
||||
unlinkSync(VERSION_MARKER_PATH);
|
||||
} catch (unlinkError) {
|
||||
// Marker might not exist
|
||||
// Marker might not exist - expected during first run
|
||||
// Not logging - this is a normal case during initial setup
|
||||
}
|
||||
logger.error('SYSTEM', 'Native module rebuild needed - restart Claude Code to auto-fix');
|
||||
return '';
|
||||
|
||||
@@ -384,6 +384,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
|
||||
*/
|
||||
|
||||
@@ -66,6 +66,7 @@ function removePidFile(): void {
|
||||
try {
|
||||
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
||||
} catch (error) {
|
||||
// PID file removal is cleanup - log but don't fail shutdown
|
||||
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
@@ -128,6 +129,7 @@ export async function updateCursorContextForProject(projectName: string, port: n
|
||||
writeContextFile(entry.workspacePath, context);
|
||||
logger.debug('CURSOR', 'Updated context file', { projectName, workspacePath: entry.workspacePath });
|
||||
} catch (error) {
|
||||
// Context update is non-critical - log and continue
|
||||
logger.warn('CURSOR', 'Failed to update context file', { projectName, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
@@ -147,7 +149,11 @@ 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) {
|
||||
// Expected: port is free or service not responding
|
||||
// Not logging - this is called frequently for health checks
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForHealth(port: number, timeoutMs: number = 30000): Promise<boolean> {
|
||||
@@ -157,8 +163,11 @@ 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) {
|
||||
logger.debug('SYSTEM', 'Service not ready yet, will retry', {
|
||||
port,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
@@ -215,6 +224,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 +267,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 +297,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 +342,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();
|
||||
@@ -503,6 +517,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
|
||||
@@ -605,8 +620,11 @@ export class WorkerService {
|
||||
}
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 60000, stdio: 'ignore' });
|
||||
} catch {
|
||||
// Process may have already exited - continue cleanup
|
||||
} catch (error) {
|
||||
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', {
|
||||
pid,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -614,7 +632,8 @@ export class WorkerService {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// Process already exited - that's fine
|
||||
// Process already exited - expected during cleanup
|
||||
logger.debug('SYSTEM', 'Process already exited', { pid });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,25 +766,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 +838,7 @@ export class WorkerService {
|
||||
// Small delay between sessions to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} 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++;
|
||||
}
|
||||
@@ -993,7 +1003,9 @@ export class WorkerService {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Expected: process has exited
|
||||
// Not logging - this is called in a tight loop during cleanup
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -1400,8 +1412,9 @@ function configureCursorMcp(target: string): number {
|
||||
if (!config.mcpServers) {
|
||||
config.mcpServers = {};
|
||||
}
|
||||
} catch {
|
||||
} 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: {} };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`]);
|
||||
}
|
||||
|
||||
|
||||
@@ -495,9 +495,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
|
||||
|
||||
@@ -538,9 +538,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
|
||||
|
||||
@@ -51,8 +51,9 @@ export class PaginationHelper {
|
||||
|
||||
// Return as JSON string
|
||||
return JSON.stringify(strippedPaths);
|
||||
} catch (error) {
|
||||
// If parsing fails, return original string
|
||||
} catch (err) {
|
||||
// Expected: file paths may not be valid JSON (plain string)
|
||||
// Not logging - normal fallback for non-JSON file path strings
|
||||
return filePathsStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -476,9 +469,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
|
||||
|
||||
@@ -1400,7 +1400,9 @@ export class SearchManager {
|
||||
if (Array.isArray(filesRead) && filesRead.length > 0) {
|
||||
lines.push(`**Files Read:** ${filesRead.join(', ')}`);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Expected: files_read may not be valid JSON (plain string)
|
||||
// Not logging - normal fallback for plain text file lists
|
||||
if (summary.files_read.trim()) {
|
||||
lines.push(`**Files Read:** ${summary.files_read}`);
|
||||
}
|
||||
@@ -1414,7 +1416,9 @@ export class SearchManager {
|
||||
if (Array.isArray(filesEdited) && filesEdited.length > 0) {
|
||||
lines.push(`**Files Edited:** ${filesEdited.join(', ')}`);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Expected: files_edited may not be valid JSON (plain string)
|
||||
// Not logging - normal fallback for plain text file lists
|
||||
if (summary.files_edited.trim()) {
|
||||
lines.push(`**Files Edited:** ${summary.files_edited}`);
|
||||
}
|
||||
@@ -1696,6 +1700,7 @@ export class SearchManager {
|
||||
}]
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.error('SEARCH', 'Timeline query failed', { query, anchor }, error);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
|
||||
@@ -286,7 +286,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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -102,7 +102,9 @@ export function getCurrentProjectName(): string {
|
||||
windowsHide: true
|
||||
}).trim();
|
||||
return basename(gitRoot);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Expected: not a git repo or git not available
|
||||
// Not logging - this is a common fallback path
|
||||
return basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export function parseJsonArray(json: string | null): string[] {
|
||||
const parsed = JSON.parse(json);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (err) {
|
||||
// [APPROVED OVERRIDE]: Expected JSON parse failures for malformed data fields, too frequent to log
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -2472,6 +2472,177 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/* Responsive Modal */
|
||||
@media (max-width: 900px) {
|
||||
.modal-body {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
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(300); // Default height
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startYRef = useRef(0);
|
||||
const startHeightRef = useRef(0);
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
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) {
|
||||
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]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 console-clear-btn"
|
||||
onClick={handleClearLogs}
|
||||
disabled={isLoading}
|
||||
title="Clear logs"
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
<button
|
||||
className="console-control-btn"
|
||||
onClick={onClose}
|
||||
title="Close console"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="console-error">
|
||||
⚠ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="console-content">
|
||||
<pre className="console-logs">
|
||||
{logs || 'No logs available'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -42,6 +42,8 @@ export class TranscriptParser {
|
||||
const entry = JSON.parse(line) as TranscriptEntry;
|
||||
this.entries.push(entry);
|
||||
} catch (error) {
|
||||
// Note: Parse errors are accumulated and accessible via getParseErrors()
|
||||
// Not logging each individual line failure - would be too verbose for large transcripts
|
||||
this.parseErrors.push({
|
||||
lineNumber: index + 1,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user