claude-roi 0.3.4 → 0.5.0

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/README.md CHANGED
@@ -116,11 +116,11 @@ Parsed session data is cached at `~/.cache/claude-roi/parsed-sessions.json`. On
116
116
 
117
117
  ### Cost Calculation
118
118
 
119
- Token costs are version-aware and calculated per model:
119
+ Token costs are version-aware and calculated per model (see [Anthropic pricing](https://platform.claude.com/docs/en/about-claude/pricing)):
120
120
 
121
121
  | Model | Input | Output | Cache Read | Cache Write |
122
122
  | --- | --- | --- | --- | --- |
123
- | Opus 4.6 | $15/M | $75/M | $1.50/M | $18.75/M |
123
+ | Opus 4.6 | $5/M | $25/M | $0.50/M | $6.25/M |
124
124
  | Opus 4.5 | $5/M | $25/M | $0.50/M | $6.25/M |
125
125
  | Opus 4.0/4.1 (legacy) | $15/M | $75/M | $1.50/M | $18.75/M |
126
126
  | Sonnet 3.7/4.0/4.5/4.6 | $3/M | $15/M | $0.30/M | $3.75/M |
@@ -161,7 +161,7 @@ git push --follow-tags
161
161
 
162
162
  This automatically publishes to npm and creates a GitHub Release with auto-generated notes.
163
163
 
164
- **Setup (one-time):** Add an `NPM_TOKEN` secret in your repo settings (Settings → Secrets → Actions) with a [granular access token](https://www.npmjs.com/settings/~/tokens/granular-access-tokens/new) that has read/write access to the `claude-roi` package.
164
+ **Setup (one-time):** Configure [trusted publishing](https://docs.npmjs.com/trusted-publishers/) on npm for the `claude-roi` package, linking it to the GitHub Actions workflow. No tokens or secrets needed.
165
165
 
166
166
  ## Contributing
167
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-roi",
3
- "version": "0.3.4",
3
+ "version": "0.5.0",
4
4
  "description": "Correlate Claude Code token usage with git output to measure AI coding agent ROI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1352,6 +1352,7 @@
1352
1352
  <div class="meta-info">
1353
1353
  <span class="badge" id="date-range"></span>
1354
1354
  <span class="badge">All data stays local</span>
1355
+ <span class="badge">No tracking, no telemetry</span>
1355
1356
  </div>
1356
1357
  </header>
1357
1358
 
@@ -1516,7 +1517,11 @@ document.addEventListener('DOMContentLoaded', async () => {
1516
1517
 
1517
1518
  function render() {
1518
1519
  const d = DATA;
1519
- document.getElementById('date-range').textContent = `Last ${d.meta.daysAnalyzed} days`;
1520
+ const fmtDate = iso => new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
1521
+ const dateLabel = (d.meta.startDate && d.meta.endDate)
1522
+ ? `${fmtDate(d.meta.startDate)} – ${fmtDate(d.meta.endDate)}`
1523
+ : `Last ${d.meta.daysAnalyzed} days`;
1524
+ document.getElementById('date-range').textContent = dateLabel;
1520
1525
  const summary = d.summary;
1521
1526
  const t = d.tokenAnalytics;
1522
1527
 
@@ -2187,7 +2192,17 @@ function initCharts() {
2187
2192
  const model = modelLabels[ctx.dataIndex];
2188
2193
  const d = models[model];
2189
2194
  const tpc = d.tokensPerCommit ? formatTokens(d.tokensPerCommit) + ' tok/commit' : 'no commits';
2190
- return ` $${d.cost.toFixed(2)} | ${d.sessions} sessions | ${d.commits} commits | ${tpc}`;
2195
+ const lines = [
2196
+ ` $${d.cost.toFixed(2)} | ${formatTokens(d.tokens)} tokens | ${d.sessions} sessions | ${Math.round(d.commits)} commits | ${tpc}`,
2197
+ ];
2198
+ const subEntries = Object.entries(d.subModels || {});
2199
+ if (subEntries.length >= 1) {
2200
+ for (const [modelId, sub] of subEntries) {
2201
+ const pct = d.cost > 0 ? Math.round((sub.cost / d.cost) * 100) : 0;
2202
+ lines.push(` ${formatModelName(modelId)}: $${sub.cost.toFixed(2)} | ${formatTokens(sub.tokens)} tokens (${pct}%)`);
2203
+ }
2204
+ }
2205
+ return lines;
2191
2206
  },
2192
2207
  },
2193
2208
  },
package/src/metrics.js CHANGED
@@ -420,11 +420,18 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
420
420
  for (const [model, data] of Object.entries(session.modelBreakdown)) {
421
421
  const family = getModelFamily(model) || 'unknown';
422
422
  if (!modelBreakdown[family]) {
423
- modelBreakdown[family] = { cost: 0, tokens: 0, sessions: 0, commits: 0, avgCostPerCommit: null };
423
+ modelBreakdown[family] = { cost: 0, tokens: 0, sessions: 0, commits: 0, avgCostPerCommit: null, subModels: {} };
424
424
  }
425
425
  modelBreakdown[family].cost += data.cost;
426
426
  modelBreakdown[family].tokens += data.tokens;
427
427
 
428
+ // Accumulate sub-model cost and tokens within this family
429
+ if (!modelBreakdown[family].subModels[model]) {
430
+ modelBreakdown[family].subModels[model] = { cost: 0, tokens: 0 };
431
+ }
432
+ modelBreakdown[family].subModels[model].cost += data.cost;
433
+ modelBreakdown[family].subModels[model].tokens += data.tokens;
434
+
428
435
  // Distribute sessions and commits proportionally by token share
429
436
  const share = sessionTotalTokens > 0 ? data.tokens / sessionTotalTokens : 0;
430
437
  modelBreakdown[family].sessions += share;
@@ -435,6 +442,9 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
435
442
  data.sessions = Math.round(data.sessions);
436
443
  data.avgCostPerCommit = data.commits > 0 ? data.cost / data.commits : null;
437
444
  data.tokensPerCommit = data.commits > 0 ? Math.round(data.tokens / data.commits) : null;
445
+ data.subModels = Object.fromEntries(
446
+ Object.entries(data.subModels).sort(([, a], [, b]) => b.cost - a.cost)
447
+ );
438
448
  }
439
449
 
440
450
  // ---- Tool breakdown ----
@@ -563,6 +573,12 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
563
573
  meta: {
564
574
  generatedAt: new Date().toISOString(),
565
575
  daysAnalyzed: days,
576
+ startDate: correlatedSessions.length > 0
577
+ ? new Date(Math.min(...correlatedSessions.map(s => new Date(s.startTime).getTime()))).toISOString()
578
+ : null,
579
+ endDate: correlatedSessions.length > 0
580
+ ? new Date(Math.max(...correlatedSessions.map(s => new Date(s.startTime).getTime()))).toISOString()
581
+ : null,
566
582
  defaultBranches: Object.fromEntries(
567
583
  Object.entries(commitsByRepo).map(([repo, a]) => [repo.split('/').pop(), a.defaultBranch]).filter(([, b]) => b)
568
584
  ),