Merge branch 'main' into feature/hybrid-search

Resolved conflicts by:
- Keeping feature/hybrid-search build process documentation in CLAUDE.md
- Removing deleted plugin/scripts/search-server.js (intentionally deleted in feature branch)
- Removing usage logging from worker-service.ts (telemetry captured at SDK level)
- Rebuilt worker-service.cjs after resolving source file conflicts

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-11-03 19:15:18 -05:00
12 changed files with 563 additions and 744 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "4.3.3",
"version": "4.3.4",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+44 -2
View File
@@ -4,7 +4,7 @@
Claude-mem is a persistent memory compression system that preserves context across Claude Code sessions. It automatically captures tool usage observations, processes them through the Claude Agent SDK, and makes summaries available to future sessions.
**Current Version**: 4.3.3
**Current Version**: 4.3.4
**License**: AGPL-3.0
**Author**: Alex Newman (@thedotmack)
@@ -189,6 +189,36 @@ Tool Execution → Hook Capture → Worker Processing → AI Compression → Dat
Search Query → MCP Server → SessionSearch → FTS5 Query → Results with Citations
```
### Usage Tracking
Claude-mem automatically tracks SDK usage metrics to JSONL files for cost analysis:
**Location**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl`
**Captured Metrics**:
- Token counts (input, output, cache creation, cache read)
- Total cost in USD per API call
- Duration metrics (total time and API time)
- Number of turns per session
- Session and project attribution
- Model information
**Analysis Tools**:
```bash
# Analyze today's usage
npm run usage:today
# Analyze specific date
npm run usage:analyze 2025-11-03
```
The analysis script provides:
- Total cost and token usage
- Cache hit rates and savings
- Cost breakdowns by project
- Cost breakdowns by model
- Average cost per API call
## Development
### Directory Structure
@@ -281,10 +311,22 @@ This approach is especially valuable when:
For detailed version history and changelog, see [CHANGELOG.md](CHANGELOG.md).
**Current Version**: 4.3.3
**Current Version**: 4.3.4
### Recent Highlights
#### v4.3.4 (2025-11-01)
**Breaking Changes**: None (patch version)
**Fixes**:
- Fixed SessionStart hooks running on session resume (plugin/hooks/hooks.json:4)
- Added matcher configuration to only run SessionStart hooks on startup, clear, or compact events
- Prevents unnecessary hook execution and improves performance on session resume
**Technical Details**:
- Modified: plugin/hooks/hooks.json:4 (added `"matcher": "startup|clear|compact"`)
- Impact: Hooks now skip execution when resuming existing sessions
#### v4.3.3 (2025-10-27)
**Breaking Changes**: None (patch version)
+1 -1
View File
@@ -22,7 +22,7 @@
<a href="package.json">
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node">
</a>
<a href="https://github.com/hesreallyhim/awesome-claude-code">
<a href="https://github.com/thedotmack/awesome-claude-code">
<img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Claude Code">
</a>
</p>
@@ -0,0 +1,61 @@
For tracking costs and tokens in your Agent SDK plugin, you have built-in programmatic access to usage data through the SDK itself[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking).
## Agent SDK Cost Tracking
The Claude Agent SDK provides detailed token usage information for each interaction[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking). Here's how to track it:
**TypeScript:**
```typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
const result = await query({
prompt: "Your task here",
options: {
onMessage: (message) => {
if (message.type === 'assistant' && message.usage) {
console.log(`Message ID: ${message.id}`);
console.log(`Usage:`, message.usage);
}
}
}
});
```
[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking)
The final `result` message contains the total cumulative usage from all steps in the conversation[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking):
```typescript
console.log("Total usage:", result.usage);
console.log("Total cost:", result.usage.total_cost_usd);
```
[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking)
## Important: Avoid Double-Counting
When Claude executes tools in parallel, multiple assistant messages may share the same ID and usage data[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking). You should only charge once per unique message ID[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking):
```typescript
const processedMessageIds = new Set<string>();
onMessage: (message) => {
if (message.type === 'assistant' && message.usage) {
// Skip if already processed
if (processedMessageIds.has(message.id)) {
return;
}
processedMessageIds.add(message.id);
// Record usage here
}
}
```
[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking)
## Usage Fields
Each usage object contains[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking):
- `input_tokens`: Base input tokens processed
- `output_tokens`: Tokens generated in the response
- `cache_creation_input_tokens`: Tokens used to create cache entries
- `cache_read_input_tokens`: Tokens read from cache
- `total_cost_usd`: Total cost in USD (only in result message)
+13 -5
View File
@@ -11,9 +11,9 @@ Claude-Mem works automatically once installed. No manual intervention required!
### The Full Cycle
1. **Start Claude Code** - Context from last 3 sessions appears automatically
1. **Start Claude Code** - Context from last 10 sessions appears automatically
2. **Work normally** - Every tool execution is captured
3. **Stop Claude** - Summary is generated and saved
3. **Claude finishes responding** - Stop hook automatically generates and saves a summary
4. **Next session** - Previous work appears in context
### What Gets Captured
@@ -42,7 +42,7 @@ The worker service processes tool observations and extracts:
### Session Summaries
When you stop Claude (or a session ends), a summary is generated with:
When Claude finishes responding (triggering the Stop hook), a summary is automatically generated with:
- **Request** - What you asked for
- **Investigated** - What Claude explored
@@ -159,7 +159,7 @@ Context injection uses three-tier verbosity for efficient token usage:
This ensures you get maximum detail for recent work while still having context from older sessions.
## Multi-Prompt Sessions
## Multi-Prompt Sessions & `/clear` Behavior
Claude-Mem supports sessions that span multiple user prompts:
@@ -167,7 +167,15 @@ Claude-Mem supports sessions that span multiple user prompts:
- **prompt_number**: Identifies specific prompt within session
- **Session continuity**: Observations and summaries link across prompts
When you use `/clear`, the session doesn't end - it continues with a new prompt number. This preserves context across conversation restarts.
### Important Note About `/clear`
When you use `/clear`, the session doesn't end - it continues with a new prompt number. This means:
- ✅ **Context is re-injected** from recent sessions (SessionStart hook fires with `source: "clear"`)
- ✅ **Observations are still being captured** and added to the current session
- ✅ **A summary will be generated** when Claude finishes responding (Stop hook fires)
The `/clear` command clears the conversation context visible to Claude AND re-injects fresh context from recent sessions, while the underlying session continues tracking observations.
## Next Steps
+234 -728
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "4.3.3",
"version": "4.3.4",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
@@ -39,7 +39,9 @@
"worker:start": "pm2 start ecosystem.config.cjs",
"worker:stop": "pm2 stop claude-mem-worker",
"worker:restart": "pm2 restart claude-mem-worker",
"worker:logs": "pm2 logs claude-mem-worker"
"worker:logs": "pm2 logs claude-mem-worker",
"usage:analyze": "node scripts/analyze-usage.js",
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
@@ -48,14 +50,14 @@
"express": "^4.18.2",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
"pm2": "^5.3.0",
"pm2": "^6.0.13",
"zod-to-json-schema": "^3.24.6"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.8",
"@types/express": "^4.17.21",
"@types/node": "^20.0.0",
"esbuild": "^0.20.0",
"esbuild": "^0.25.12",
"tsx": "^4.20.6",
"typescript": "^5.3.0"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "4.3.3",
"version": "4.3.4",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+2 -2
View File
@@ -591,8 +591,8 @@ IMPORTANT: This is not the end of the session. You will receive more requests to
WHERE up.claude_session_id = ?
ORDER BY up.created_at_epoch DESC
LIMIT 1
`).get(c);o.close(),p&&this.chromaSync.syncUserPrompt(p.id,p.sdk_session_id,p.project,p.prompt_text,p.prompt_number,p.created_at_epoch).catch(f=>{Y.failure("WORKER","Failed to sync user_prompt to Chroma",{promptId:p.id},f),process.exit(1)}),u.generatorPromise=this.runSDKAgent(u).catch(f=>{Y.failure("WORKER","SDK agent error",{sessionId:t},f);let d=new Pr;d.markSessionFailed(t),d.close(),this.sessions.delete(t)}),Y.success("WORKER","Session initialized",{sessionId:t,port:this.port}),a.json({status:"initialized",sessionDbId:t,port:this.port})}handleObservation(e,a){let t=parseInt(e.params.sessionDbId,10),{tool_name:s,tool_input:i,tool_output:n,prompt_number:o}=e.body,l=this.sessions.get(t);if(!l){let p=new Pr,f=p.getSessionById(t);p.close(),l={sessionDbId:t,claudeSessionId:f.claude_session_id,sdkSessionId:null,project:f.project,userPrompt:f.user_prompt,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,l),l.generatorPromise=this.runSDKAgent(l).catch(d=>{Y.failure("WORKER","SDK agent error",{sessionId:t},d);let v=new Pr;v.markSessionFailed(t),v.close(),this.sessions.delete(t)})}l.observationCounter++;let c=Y.correlationId(t,l.observationCounter),u=Y.formatTool(s,i);Y.dataIn("WORKER",`Observation queued: ${u}`,{correlationId:c,queue:l.pendingMessages.length+1}),l.pendingMessages.push({type:"observation",tool_name:s,tool_input:i,tool_output:n,prompt_number:o}),a.json({status:"queued",queueLength:l.pendingMessages.length})}handleSummarize(e,a){let t=parseInt(e.params.sessionDbId,10),{prompt_number:s}=e.body,i=this.sessions.get(t);if(!i){let n=new Pr,o=n.getSessionById(t);n.close(),i={sessionDbId:t,claudeSessionId:o.claude_session_id,sdkSessionId:null,project:o.project,userPrompt:o.user_prompt,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,i),i.generatorPromise=this.runSDKAgent(i).catch(l=>{Y.failure("WORKER","SDK agent error",{sessionId:t},l);let c=new Pr;c.markSessionFailed(t),c.close(),this.sessions.delete(t)})}Y.dataIn("WORKER","Summary requested",{sessionId:t,promptNumber:s,queue:i.pendingMessages.length+1}),i.pendingMessages.push({type:"summarize",prompt_number:s}),a.json({status:"queued",queueLength:i.pendingMessages.length})}handleStatus(e,a){let t=parseInt(e.params.sessionDbId,10),s=this.sessions.get(t);if(!s){a.status(404).json({error:"Session not found"});return}a.json({sessionDbId:t,sdkSessionId:s.sdkSessionId,project:s.project,pendingMessages:s.pendingMessages.length})}async handleDelete(e,a){let t=parseInt(e.params.sessionDbId,10),s=this.sessions.get(t);if(!s){a.status(404).json({error:"Session not found"});return}Y.warn("WORKER","Session delete requested",{sessionId:t}),s.abortController.abort(),s.generatorPromise&&await Promise.race([s.generatorPromise,new Promise(n=>setTimeout(n,5e3))]);let i=new Pr;i.markSessionFailed(t),i.close(),this.sessions.delete(t),Y.info("WORKER","Session deleted",{sessionId:t}),a.json({status:"deleted"})}async runSDKAgent(e){Y.info("SDK","Agent starting",{sessionId:e.sessionDbId});let a=V5();Y.info("SDK",`Using Claude executable: ${a}`,{sessionId:e.sessionDbId});try{let t=Ab({prompt:this.createMessageGenerator(e),options:{model:H5,disallowedTools:B5,abortController:e.abortController,pathToClaudeCodeExecutable:a}});for await(let n of t)if(n.type==="assistant"){let o=n.message.content,l=Array.isArray(o)?o.filter(u=>u.type==="text").map(u=>u.text).join(`
`):typeof o=="string"?o:"",c=l.length;Y.dataOut("SDK",`Response received (${c} chars)`,{sessionId:e.sessionDbId,promptNumber:e.lastPromptNumber}),Y.debug("SDK","Full response",{sessionId:e.sessionDbId},l),this.handleAgentMessage(e,l,e.lastPromptNumber)}let s=Date.now()-e.startTime;Y.success("SDK","Agent completed",{sessionId:e.sessionDbId,duration:`${(s/1e3).toFixed(1)}s`});let i=new Pr;i.markSessionCompleted(e.sessionDbId),i.close(),this.sessions.delete(e.sessionDbId)}catch(t){throw t.name==="AbortError"?Y.warn("SDK","Agent aborted",{sessionId:e.sessionDbId}):Y.failure("SDK","Agent error",{sessionId:e.sessionDbId},t),t}}async*createMessageGenerator(e){let a=cw(e.project,e.claudeSessionId,e.userPrompt);for(Y.dataIn("SDK",`Init prompt sent (${a.length} chars)`,{sessionId:e.sessionDbId,claudeSessionId:e.claudeSessionId,project:e.project}),Y.debug("SDK","Full init prompt",{sessionId:e.sessionDbId},a),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:a}};!e.abortController.signal.aborted;){if(e.pendingMessages.length===0){await new Promise(t=>setTimeout(t,100));continue}for(;e.pendingMessages.length>0;){let t=e.pendingMessages.shift();if(t.type==="summarize"){e.lastPromptNumber=t.prompt_number;let s=new Pr,i=s.getSessionById(e.sessionDbId);s.close();let n=uw(i);Y.dataIn("SDK",`Summary prompt sent (${n.length} chars)`,{sessionId:e.sessionDbId,promptNumber:t.prompt_number}),Y.debug("SDK","Full summary prompt",{sessionId:e.sessionDbId},n),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:n}}}else if(t.type==="observation"){e.lastPromptNumber=t.prompt_number;let s=lw({id:0,tool_name:t.tool_name,tool_input:t.tool_input,tool_output:t.tool_output,created_at_epoch:Date.now()}),i=Y.formatTool(t.tool_name,t.tool_input),n=Y.correlationId(e.sessionDbId,e.observationCounter);Y.dataIn("SDK",`Observation prompt: ${i}`,{correlationId:n,promptNumber:t.prompt_number,size:`${s.length} chars`}),Y.debug("SDK","Full observation prompt",{correlationId:n},s),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:s}}}}}}handleAgentMessage(e,a,t){let s=Y.correlationId(e.sessionDbId,e.observationCounter);Y.info("PARSER",`Processing response (${a.length} chars)`,{sessionId:e.sessionDbId,promptNumber:t,preview:a.substring(0,200)});let i=pw(a,s);i.length>0&&Y.info("PARSER",`Parsed ${i.length} observation(s)`,{correlationId:s,promptNumber:t,types:i.map(l=>l.type).join(", ")});let n=new Pr;for(let l of i){let{id:c,createdAtEpoch:u}=n.storeObservation(e.claudeSessionId,e.project,l,t);Y.success("DB","Observation stored",{correlationId:s,type:l.type,title:l.title,id:c}),this.chromaSync.syncObservation(c,e.claudeSessionId,e.project,l,t,u).then(()=>{Y.success("CHROMA","Observation synced",{correlationId:s,observationId:c})}).catch(p=>{Y.error("CHROMA","Observation sync failed - crashing worker",{correlationId:s,observationId:c},p),process.exit(1)})}Y.info("PARSER","Looking for summary tags...",{sessionId:e.sessionDbId});let o=dw(a,e.sessionDbId);if(o){Y.success("PARSER","Summary parsed successfully!",{sessionId:e.sessionDbId,promptNumber:t,hasRequest:!!o.request,hasInvestigated:!!o.investigated,hasLearned:!!o.learned,hasCompleted:!!o.completed,hasNextSteps:!!o.next_steps});let{id:l,createdAtEpoch:c}=n.storeSummary(e.claudeSessionId,e.project,o,t);Y.success("DB","\u{1F4DD} SUMMARY STORED IN DATABASE",{sessionId:e.sessionDbId,promptNumber:t,id:l}),this.chromaSync.syncSummary(l,e.claudeSessionId,e.project,o,t,c).then(()=>{Y.success("CHROMA","Summary synced",{sessionId:e.sessionDbId,summaryId:l})}).catch(u=>{Y.error("CHROMA","Summary sync failed - crashing worker",{sessionId:e.sessionDbId,summaryId:l},u),process.exit(1)})}else Y.warn("PARSER","NO SUMMARY TAGS FOUND in response",{sessionId:e.sessionDbId,promptNumber:t,contentSample:a.substring(0,500)});n.close()}};async function Z5(){await new Hc().start(),process.on("SIGINT",()=>{Y.warn("SYSTEM","Shutting down (SIGINT)"),process.exit(0)}),process.on("SIGTERM",()=>{Y.warn("SYSTEM","Shutting down (SIGTERM)"),process.exit(0)})}Z5().catch(r=>{Y.failure("SYSTEM","Fatal startup error",{},r),process.exit(1)});0&&(module.exports={WorkerService});
`).get(c);o.close(),p&&this.chromaSync.syncUserPrompt(p.id,p.sdk_session_id,p.project,p.prompt_text,p.prompt_number,p.created_at_epoch).catch(f=>{Y.failure("WORKER","Failed to sync user_prompt to Chroma",{promptId:p.id},f),process.exit(1)}),u.generatorPromise=this.runSDKAgent(u).catch(f=>{Y.failure("WORKER","SDK agent error",{sessionId:t},f);let d=new Pr;d.markSessionFailed(t),d.close(),this.sessions.delete(t)}),Y.success("WORKER","Session initialized",{sessionId:t,port:this.port}),a.json({status:"initialized",sessionDbId:t,port:this.port})}handleObservation(e,a){let t=parseInt(e.params.sessionDbId,10),{tool_name:s,tool_input:i,tool_output:n,prompt_number:o}=e.body,l=this.sessions.get(t);if(!l){let p=new Pr,f=p.getSessionById(t);p.close(),l={sessionDbId:t,claudeSessionId:f.claude_session_id,sdkSessionId:null,project:f.project,userPrompt:f.user_prompt,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,l),l.generatorPromise=this.runSDKAgent(l).catch(d=>{Y.failure("WORKER","SDK agent error",{sessionId:t},d);let v=new Pr;v.markSessionFailed(t),v.close(),this.sessions.delete(t)})}l.observationCounter++;let c=Y.correlationId(t,l.observationCounter),u=Y.formatTool(s,i);Y.dataIn("WORKER",`Observation queued: ${u}`,{correlationId:c,queue:l.pendingMessages.length+1}),l.pendingMessages.push({type:"observation",tool_name:s,tool_input:i,tool_output:n,prompt_number:o}),a.json({status:"queued",queueLength:l.pendingMessages.length})}handleSummarize(e,a){let t=parseInt(e.params.sessionDbId,10),{prompt_number:s}=e.body,i=this.sessions.get(t);if(!i){let n=new Pr,o=n.getSessionById(t);n.close(),i={sessionDbId:t,claudeSessionId:o.claude_session_id,sdkSessionId:null,project:o.project,userPrompt:o.user_prompt,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,i),i.generatorPromise=this.runSDKAgent(i).catch(l=>{Y.failure("WORKER","SDK agent error",{sessionId:t},l);let c=new Pr;c.markSessionFailed(t),c.close(),this.sessions.delete(t)})}Y.dataIn("WORKER","Summary requested",{sessionId:t,promptNumber:s,queue:i.pendingMessages.length+1}),i.pendingMessages.push({type:"summarize",prompt_number:s}),a.json({status:"queued",queueLength:i.pendingMessages.length})}handleStatus(e,a){let t=parseInt(e.params.sessionDbId,10),s=this.sessions.get(t);if(!s){a.status(404).json({error:"Session not found"});return}a.json({sessionDbId:t,sdkSessionId:s.sdkSessionId,project:s.project,pendingMessages:s.pendingMessages.length})}async handleDelete(e,a){let t=parseInt(e.params.sessionDbId,10),s=this.sessions.get(t);if(!s){a.status(404).json({error:"Session not found"});return}Y.warn("WORKER","Session delete requested",{sessionId:t}),s.abortController.abort(),s.generatorPromise&&await Promise.race([s.generatorPromise,new Promise(n=>setTimeout(n,5e3))]);let i=new Pr;i.markSessionFailed(t),i.close(),this.sessions.delete(t),Y.info("WORKER","Session deleted",{sessionId:t}),a.json({status:"deleted"})}async runSDKAgent(e){Y.info("SDK","Agent starting",{sessionId:e.sessionDbId});let a=V5();Y.info("SDK",`Using Claude executable: ${a}`,{sessionId:e.sessionDbId});try{let t=Ab({prompt:this.createMessageGenerator(e),options:{model:H5,disallowedTools:B5,abortController:e.abortController,pathToClaudeCodeExecutable:a}});for await(let n of t){if(n.type==="assistant"){let o=n.message.content,l=Array.isArray(o)?o.filter(u=>u.type==="text").map(u=>u.text).join(`
`):typeof o=="string"?o:"",c=l.length;Y.dataOut("SDK",`Response received (${c} chars)`,{sessionId:e.sessionDbId,promptNumber:e.lastPromptNumber}),Y.debug("SDK","Full response",{sessionId:e.sessionDbId},l),this.handleAgentMessage(e,l,e.lastPromptNumber)}n.type==="result"&&n.subtype}let s=Date.now()-e.startTime;Y.success("SDK","Agent completed",{sessionId:e.sessionDbId,duration:`${(s/1e3).toFixed(1)}s`});let i=new Pr;i.markSessionCompleted(e.sessionDbId),i.close(),this.sessions.delete(e.sessionDbId)}catch(t){throw t.name==="AbortError"?Y.warn("SDK","Agent aborted",{sessionId:e.sessionDbId}):Y.failure("SDK","Agent error",{sessionId:e.sessionDbId},t),t}}async*createMessageGenerator(e){let a=cw(e.project,e.claudeSessionId,e.userPrompt);for(Y.dataIn("SDK",`Init prompt sent (${a.length} chars)`,{sessionId:e.sessionDbId,claudeSessionId:e.claudeSessionId,project:e.project}),Y.debug("SDK","Full init prompt",{sessionId:e.sessionDbId},a),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:a}};!e.abortController.signal.aborted;){if(e.pendingMessages.length===0){await new Promise(t=>setTimeout(t,100));continue}for(;e.pendingMessages.length>0;){let t=e.pendingMessages.shift();if(t.type==="summarize"){e.lastPromptNumber=t.prompt_number;let s=new Pr,i=s.getSessionById(e.sessionDbId);s.close();let n=uw(i);Y.dataIn("SDK",`Summary prompt sent (${n.length} chars)`,{sessionId:e.sessionDbId,promptNumber:t.prompt_number}),Y.debug("SDK","Full summary prompt",{sessionId:e.sessionDbId},n),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:n}}}else if(t.type==="observation"){e.lastPromptNumber=t.prompt_number;let s=lw({id:0,tool_name:t.tool_name,tool_input:t.tool_input,tool_output:t.tool_output,created_at_epoch:Date.now()}),i=Y.formatTool(t.tool_name,t.tool_input),n=Y.correlationId(e.sessionDbId,e.observationCounter);Y.dataIn("SDK",`Observation prompt: ${i}`,{correlationId:n,promptNumber:t.prompt_number,size:`${s.length} chars`}),Y.debug("SDK","Full observation prompt",{correlationId:n},s),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:s}}}}}}handleAgentMessage(e,a,t){let s=Y.correlationId(e.sessionDbId,e.observationCounter);Y.info("PARSER",`Processing response (${a.length} chars)`,{sessionId:e.sessionDbId,promptNumber:t,preview:a.substring(0,200)});let i=pw(a,s);i.length>0&&Y.info("PARSER",`Parsed ${i.length} observation(s)`,{correlationId:s,promptNumber:t,types:i.map(l=>l.type).join(", ")});let n=new Pr;for(let l of i){let{id:c,createdAtEpoch:u}=n.storeObservation(e.claudeSessionId,e.project,l,t);Y.success("DB","Observation stored",{correlationId:s,type:l.type,title:l.title,id:c}),this.chromaSync.syncObservation(c,e.claudeSessionId,e.project,l,t,u).then(()=>{Y.success("CHROMA","Observation synced",{correlationId:s,observationId:c})}).catch(p=>{Y.error("CHROMA","Observation sync failed - crashing worker",{correlationId:s,observationId:c},p),process.exit(1)})}Y.info("PARSER","Looking for summary tags...",{sessionId:e.sessionDbId});let o=dw(a,e.sessionDbId);if(o){Y.success("PARSER","Summary parsed successfully!",{sessionId:e.sessionDbId,promptNumber:t,hasRequest:!!o.request,hasInvestigated:!!o.investigated,hasLearned:!!o.learned,hasCompleted:!!o.completed,hasNextSteps:!!o.next_steps});let{id:l,createdAtEpoch:c}=n.storeSummary(e.claudeSessionId,e.project,o,t);Y.success("DB","\u{1F4DD} SUMMARY STORED IN DATABASE",{sessionId:e.sessionDbId,promptNumber:t,id:l}),this.chromaSync.syncSummary(l,e.claudeSessionId,e.project,o,t,c).then(()=>{Y.success("CHROMA","Summary synced",{sessionId:e.sessionDbId,summaryId:l})}).catch(u=>{Y.error("CHROMA","Summary sync failed - crashing worker",{sessionId:e.sessionDbId,summaryId:l},u),process.exit(1)})}else Y.warn("PARSER","NO SUMMARY TAGS FOUND in response",{sessionId:e.sessionDbId,promptNumber:t,contentSample:a.substring(0,500)});n.close()}};async function Z5(){await new Hc().start(),process.on("SIGINT",()=>{Y.warn("SYSTEM","Shutting down (SIGINT)"),process.exit(0)}),process.on("SIGTERM",()=>{Y.warn("SYSTEM","Shutting down (SIGTERM)"),process.exit(0)})}Z5().catch(r=>{Y.failure("SYSTEM","Fatal startup error",{},r),process.exit(1)});0&&(module.exports={WorkerService});
/*! Bundled license information:
depd/index.js:
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env node
/**
* Analyze usage logs from ~/.claude-mem/usage-logs/
*
* Usage:
* node scripts/analyze-usage.js [date]
*
* Example:
* node scripts/analyze-usage.js 2025-11-03
* node scripts/analyze-usage.js # Uses today's date
*/
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const usageDir = join(homedir(), '.claude-mem', 'usage-logs');
// Get date from command line or use today
const targetDate = process.argv[2] || new Date().toISOString().split('T')[0];
const filename = `usage-${targetDate}.jsonl`;
const filepath = join(usageDir, filename);
console.log(`\n📊 Usage Analysis for ${targetDate}\n`);
console.log(`Reading from: ${filepath}\n`);
try {
const content = readFileSync(filepath, 'utf-8');
const lines = content.trim().split('\n');
let totalCost = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
let totalCacheCreation = 0;
let totalCacheRead = 0;
const projectStats = {};
const modelStats = {};
lines.forEach(line => {
if (!line.trim()) return;
try {
const entry = JSON.parse(line);
// Aggregate totals
totalCost += entry.totalCostUsd || 0;
totalInputTokens += entry.usage?.inputTokens || 0;
totalOutputTokens += entry.usage?.outputTokens || 0;
totalCacheCreation += entry.usage?.cacheCreationInputTokens || 0;
totalCacheRead += entry.usage?.cacheReadInputTokens || 0;
// Project stats
if (!projectStats[entry.project]) {
projectStats[entry.project] = {
cost: 0,
sessions: new Set(),
tokens: 0
};
}
projectStats[entry.project].cost += entry.totalCostUsd || 0;
projectStats[entry.project].sessions.add(entry.sessionDbId);
projectStats[entry.project].tokens += (entry.usage?.inputTokens || 0) + (entry.usage?.outputTokens || 0);
// Model stats
if (!modelStats[entry.model]) {
modelStats[entry.model] = {
cost: 0,
calls: 0,
tokens: 0
};
}
modelStats[entry.model].cost += entry.totalCostUsd || 0;
modelStats[entry.model].calls += 1;
modelStats[entry.model].tokens += (entry.usage?.inputTokens || 0) + (entry.usage?.outputTokens || 0);
} catch (e) {
console.error(`Error parsing line: ${e.message}`);
}
});
// Print summary
console.log('═══════════════════════════════════════════════════════════\n');
console.log(`📈 Total Cost: $${totalCost.toFixed(4)}`);
console.log(`📊 Total API Calls: ${lines.length}`);
console.log(`\n🎯 Token Usage:`);
console.log(` Input Tokens: ${totalInputTokens.toLocaleString()}`);
console.log(` Output Tokens: ${totalOutputTokens.toLocaleString()}`);
console.log(` Cache Creation Tokens: ${totalCacheCreation.toLocaleString()}`);
console.log(` Cache Read Tokens: ${totalCacheRead.toLocaleString()}`);
console.log(` Total Tokens: ${(totalInputTokens + totalOutputTokens).toLocaleString()}`);
if (totalCacheRead > 0) {
const savings = ((totalCacheRead / (totalInputTokens + totalCacheRead)) * 100).toFixed(1);
console.log(` Cache Hit Rate: ${savings}%`);
}
console.log(`\n📁 By Project:`);
Object.entries(projectStats)
.sort((a, b) => b[1].cost - a[1].cost)
.forEach(([project, stats]) => {
console.log(` ${project}:`);
console.log(` Cost: $${stats.cost.toFixed(4)}`);
console.log(` Sessions: ${stats.sessions.size}`);
console.log(` Tokens: ${stats.tokens.toLocaleString()}`);
});
console.log(`\n🤖 By Model:`);
Object.entries(modelStats)
.sort((a, b) => b[1].cost - a[1].cost)
.forEach(([model, stats]) => {
console.log(` ${model}:`);
console.log(` Cost: $${stats.cost.toFixed(4)}`);
console.log(` Calls: ${stats.calls}`);
console.log(` Tokens: ${stats.tokens.toLocaleString()}`);
console.log(` Avg Cost/Call: $${(stats.cost / stats.calls).toFixed(4)}`);
});
console.log('\n═══════════════════════════════════════════════════════════\n');
} catch (error) {
if (error.code === 'ENOENT') {
console.error(`❌ No usage log found for ${targetDate}`);
console.log(`\nAvailable logs:`);
try {
const files = readdirSync(usageDir).filter(f => f.endsWith('.jsonl'));
files.forEach(f => console.log(` - ${f}`));
} catch (e) {
console.error(` Could not read usage logs directory`);
}
} else {
console.error(`❌ Error: ${error.message}`);
}
process.exit(1);
}
+5
View File
@@ -454,6 +454,11 @@ class WorkerService {
// Parse and store with prompt number (non-blocking Chroma sync)
this.handleAgentMessage(session, textContent, session.lastPromptNumber);
}
// Capture usage data from result messages
if (message.type === 'result' && message.subtype === 'success') {
// Usage telemetry is captured at SDK level
}
}
// Mark completed
+61
View File
@@ -0,0 +1,61 @@
import { appendFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
/**
* Usage data structure from Claude Agent SDK result messages
*/
export interface UsageData {
timestamp: string;
sessionDbId: number;
claudeSessionId: string;
project: string;
promptNumber: number;
model: string;
sessionId: string; // SDK session ID
uuid: string; // SDK message UUID
durationMs: number;
durationApiMs: number;
numTurns: number;
totalCostUsd: number;
usage: {
inputTokens: number;
outputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
};
}
/**
* Logger for capturing usage metrics to JSONL files
*/
export class UsageLogger {
private logDir: string;
private logFile: string;
constructor() {
this.logDir = join(homedir(), '.claude-mem', 'usage-logs');
// Create a daily log file
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
this.logFile = join(this.logDir, `usage-${date}.jsonl`);
}
/**
* Log usage data from SDK result message
*/
logUsage(data: UsageData): void {
try {
const line = JSON.stringify(data) + '\n';
appendFileSync(this.logFile, line, 'utf-8');
} catch (error) {
console.error('Failed to log usage data:', error);
}
}
/**
* Get the current log file path
*/
getLogFilePath(): string {
return this.logFile;
}
}