claude-roi 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-roi",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Correlate Claude Code token usage with git output to measure AI coding agent ROI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,5 +30,8 @@
30
30
  "express": "^5.0.0",
31
31
  "open": "^10.0.0",
32
32
  "playwright": "^1.58.2"
33
+ },
34
+ "devDependencies": {
35
+ "@playwright/test": "^1.56.1"
33
36
  }
34
37
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './tests',
5
+ timeout: 30000,
6
+ use: {
7
+ baseURL: 'http://localhost:3457',
8
+ },
9
+ });
@@ -360,8 +360,8 @@ function mergeSubagentIntoSession(parent, sub) {
360
360
  parent.cost.cacheCreationCost += sub.cost.cacheCreationCost;
361
361
  parent.cost.totalCost += sub.cost.totalCost;
362
362
 
363
- parent.assistantMessageCount += sub.assistantMessageCount;
364
- parent.userMessageCount += sub.userMessageCount;
363
+ // Message counts intentionally NOT merged — we only report
364
+ // the main-conversation messages, not internal subagent chatter.
365
365
 
366
366
  // Merge model breakdown
367
367
  for (const [model, data] of Object.entries(sub.modelBreakdown)) {
@@ -457,10 +457,28 @@ export async function parseAllProjects(claudeDir, days, projectFilter) {
457
457
  }
458
458
  }
459
459
 
460
+ // Deduplicate sessions by sessionId (same session can appear in
461
+ // multiple project dirs, e.g. main repo vs worktree paths)
462
+ const seen = new Map();
463
+ for (const s of sessions) {
464
+ const id = s.sessionId;
465
+ if (!seen.has(id)) {
466
+ seen.set(id, s);
467
+ } else {
468
+ const existing = seen.get(id);
469
+ const existingMsgs = existing.userMessageCount + existing.assistantMessageCount;
470
+ const newMsgs = s.userMessageCount + s.assistantMessageCount;
471
+ if (newMsgs > existingMsgs) {
472
+ seen.set(id, s);
473
+ }
474
+ }
475
+ }
476
+ const deduped = Array.from(seen.values());
477
+
460
478
  // Sort by start time descending
461
- sessions.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
479
+ deduped.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
462
480
 
463
- return { sessions, fileIndex };
481
+ return { sessions: deduped, fileIndex };
464
482
  }
465
483
 
466
484
  export { calculateCost, getModelFamily, PRICING };
package/src/correlator.js CHANGED
@@ -1,5 +1,19 @@
1
1
  const FALLBACK_BUFFER_MS = 2 * 60 * 60 * 1000; // 2 hours for time-only fallback
2
2
 
3
+ function computeOverlappingLines(commits, sessionFiles) {
4
+ let linesAdded = 0;
5
+ let linesDeleted = 0;
6
+ for (const c of commits) {
7
+ for (const f of c.files) {
8
+ if (sessionFiles.has(f.path)) {
9
+ linesAdded += f.added;
10
+ linesDeleted += f.deleted;
11
+ }
12
+ }
13
+ }
14
+ return { linesAdded, linesDeleted };
15
+ }
16
+
3
17
  /**
4
18
  * Correlate sessions to commits using file-based matching.
5
19
  *
@@ -55,10 +69,19 @@ export function correlateSessions(sessions, commitsByRepo) {
55
69
  claimedCommits.add(c.hash);
56
70
  }
57
71
 
58
- const linesAdded = matched.reduce((s, c) => s + c.totalAdded, 0);
59
- const linesDeleted = matched.reduce((s, c) => s + c.totalDeleted, 0);
72
+ let linesAdded, linesDeleted;
73
+ if (sessionFiles.size > 0) {
74
+ // Only count lines from files Claude actually edited
75
+ ({ linesAdded, linesDeleted } = computeOverlappingLines(matched, sessionFiles));
76
+ } else {
77
+ // Fallback (chat-only): count all lines in matched commits
78
+ linesAdded = matched.reduce((s, c) => s + c.totalAdded, 0);
79
+ linesDeleted = matched.reduce((s, c) => s + c.totalDeleted, 0);
80
+ }
60
81
  const netLines = linesAdded - linesDeleted;
61
- const filesChanged = new Set(matched.flatMap(c => c.files.map(f => f.path))).size;
82
+ const filesChanged = sessionFiles.size > 0
83
+ ? new Set(matched.flatMap(c => c.files.filter(f => sessionFiles.has(f.path)).map(f => f.path))).size
84
+ : new Set(matched.flatMap(c => c.files.map(f => f.path))).size;
62
85
  const commitsOnMain = matched.filter(c => c.onMain).length;
63
86
 
64
87
  const messageCount = session.userMessageCount + session.assistantMessageCount;