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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+234
-728
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -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,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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Executable
+134
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user