claude-roi 0.2.0 → 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 +1 -1
- package/src/claude-parser.js +22 -4
- package/src/correlator.js +26 -3
- package/src/metrics.js +9 -7
package/package.json
CHANGED
package/src/claude-parser.js
CHANGED
|
@@ -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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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 =
|
|
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;
|
package/src/metrics.js
CHANGED
|
@@ -406,6 +406,9 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
|
|
|
406
406
|
// ---- Model breakdown ----
|
|
407
407
|
const modelBreakdown = {};
|
|
408
408
|
for (const session of correlatedSessions) {
|
|
409
|
+
const sessionTotalTokens = Object.values(session.modelBreakdown)
|
|
410
|
+
.reduce((s, d) => s + d.tokens, 0);
|
|
411
|
+
|
|
409
412
|
for (const [model, data] of Object.entries(session.modelBreakdown)) {
|
|
410
413
|
const family = getModelFamily(model) || 'unknown';
|
|
411
414
|
if (!modelBreakdown[family]) {
|
|
@@ -413,16 +416,15 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
|
|
|
413
416
|
}
|
|
414
417
|
modelBreakdown[family].cost += data.cost;
|
|
415
418
|
modelBreakdown[family].tokens += data.tokens;
|
|
419
|
+
|
|
420
|
+
// Distribute sessions and commits proportionally by token share
|
|
421
|
+
const share = sessionTotalTokens > 0 ? data.tokens / sessionTotalTokens : 0;
|
|
422
|
+
modelBreakdown[family].sessions += share;
|
|
423
|
+
modelBreakdown[family].commits += session.commitCount * share;
|
|
416
424
|
}
|
|
417
|
-
// Count sessions/commits per primary model
|
|
418
|
-
const primaryFamily = getModelFamily(session.model) || 'unknown';
|
|
419
|
-
if (!modelBreakdown[primaryFamily]) {
|
|
420
|
-
modelBreakdown[primaryFamily] = { cost: 0, tokens: 0, sessions: 0, commits: 0, avgCostPerCommit: null };
|
|
421
|
-
}
|
|
422
|
-
modelBreakdown[primaryFamily].sessions++;
|
|
423
|
-
modelBreakdown[primaryFamily].commits += session.commitCount;
|
|
424
425
|
}
|
|
425
426
|
for (const data of Object.values(modelBreakdown)) {
|
|
427
|
+
data.sessions = Math.round(data.sessions);
|
|
426
428
|
data.avgCostPerCommit = data.commits > 0 ? data.cost / data.commits : null;
|
|
427
429
|
data.tokensPerCommit = data.commits > 0 ? Math.round(data.tokens / data.commits) : null;
|
|
428
430
|
}
|