cchubber 0.5.1 → 0.5.2

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": "cchubber",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "What you spent. Why you spent it. Is that normal. — Claude Code usage diagnosis with beautiful HTML reports.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -83,7 +83,7 @@ function calculateGrade(allTimeRatio, breaks, days, dailyFromJSONL, cacheHitRate
83
83
  // Based on token-optimizer's methodology (multi-signal composite)
84
84
  // but adapted for post-hoc analysis with the data CC Hubber has.
85
85
 
86
- // --- Signal 1: Cache hit rate (25%) ---
86
+ // --- Signal 1: Cache hit rate (15%) ---
87
87
  // What % of input tokens came from cache. Higher = better.
88
88
  // Thresholds from token-optimizer: >=80% = 100, >=60% = 80, >=40% = 55
89
89
  let hitRateScore;
@@ -93,7 +93,7 @@ function calculateGrade(allTimeRatio, breaks, days, dailyFromJSONL, cacheHitRate
93
93
  else if (cacheHitRate >= 40) hitRateScore = 40;
94
94
  else hitRateScore = 15;
95
95
 
96
- // --- Signal 2: Efficiency ratio (30%) ---
96
+ // --- Signal 2: Efficiency ratio (40%) ---
97
97
  // Cache reads per output token. Measures how much redundant data
98
98
  // is re-read per unit of work. Lower = more efficient.
99
99
  // Calibrated against 33 real users (median ~680).
package/src/cli/index.js CHANGED
@@ -42,7 +42,8 @@ const flags = {
42
42
  })(),
43
43
  days: (() => {
44
44
  const idx = args.indexOf('--days') !== -1 ? args.indexOf('--days') : args.indexOf('-d');
45
- return idx !== -1 && args[idx + 1] ? parseInt(args[idx + 1], 10) : 30;
45
+ const val = idx !== -1 && args[idx + 1] ? parseInt(args[idx + 1], 10) : 30;
46
+ return isNaN(val) ? 30 : val;
46
47
  })(),
47
48
  };
48
49
 
@@ -140,19 +141,15 @@ async function main() {
140
141
  if (modelRouting.available) console.log(` ✓ Model routing: ${modelRouting.opusPct}% Opus, ${modelRouting.sonnetPct}% Sonnet`);
141
142
  console.log(` ✓ ${projectBreakdown.length} projects detected`);
142
143
 
143
- // Fetch community stats for leaderboard (non-blocking, fails silently)
144
+ // Fetch community stats for leaderboard (non-blocking, 5s timeout, fails silently)
144
145
  let communityStats = null;
145
146
  try {
146
- const res = await fetch('https://cchubber-telemetry.asmirkhan087.workers.dev/stats?key=cchubber_public_readonly');
147
+ const controller = new AbortController();
148
+ const timeout = setTimeout(() => controller.abort(), 5000);
149
+ const res = await fetch('https://cchubber-telemetry.asmirkhan087.workers.dev/stats-public', { signal: controller.signal });
150
+ clearTimeout(timeout);
147
151
  if (res.ok) communityStats = await res.json();
148
152
  } catch {}
149
- // Fallback: try the private key if public isn't configured
150
- if (!communityStats) {
151
- try {
152
- const res = await fetch('https://cchubber-telemetry.asmirkhan087.workers.dev/stats?key=cchubber_x9k_private');
153
- if (res.ok) communityStats = await res.json();
154
- } catch {}
155
- }
156
153
  if (communityStats) console.log(` ✓ Community data: ${communityStats.totalReports} users from ${Object.keys(communityStats.countries || {}).length} countries`);
157
154
  else console.log(' ○ Community data unavailable (offline)');
158
155
 
@@ -1,3 +1,8 @@
1
+ function esc(s) {
2
+ if (!s) return '';
3
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
4
+ }
5
+
1
6
  export function renderHTML(report) {
2
7
  const { costAnalysis, cacheHealth, anomalies, inflection, sessionIntel, modelRouting, projectBreakdown, claudeMdStack, oauthUsage, recommendations, generatedAt } = report;
3
8
 
@@ -474,13 +479,13 @@ ${inflection && inflection.multiplier >= 1.5 ? `
474
479
  return `<div class="p-4 bg-[#0d0e0f] rounded-r-lg flex items-start gap-4" style="border-left:3px solid ${sev.border}">
475
480
  <div class="flex-1 min-w-0">
476
481
  <div class="flex items-start justify-between gap-4">
477
- <p class="text-[13px] font-semibold text-[#e3e2e3]">${r.title}</p>
482
+ <p class="text-[13px] font-semibold text-[#e3e2e3]">${esc(r.title)}</p>
478
483
  <div class="flex items-center gap-2 shrink-0">
479
484
  ${r.savings ? `<span class="text-[10px] font-mono px-2 py-0.5 rounded" style="background:${sev.border}18;color:${sev.text}">${r.savings}</span>` : ''}
480
485
  ${r.severity !== 'positive' ? `<button data-clip="${b64}" onclick="var t=atob(this.dataset.clip);navigator.clipboard.writeText(t);this.textContent='Copied!';var b=this;setTimeout(function(){b.textContent='Fix with Claude'},1500)" class="text-[10px] font-mono px-2 py-0.5 rounded border border-[rgba(70,69,84,0.3)] text-[#908fa0] cursor-pointer hover:text-[#e3e2e3] hover:border-[rgba(70,69,84,0.6)] transition-colors">Fix with Claude</button>` : ''}
481
486
  </div>
482
487
  </div>
483
- <p class="text-[11px] text-[#908fa0] mt-1 leading-relaxed">${r.action}</p>
488
+ <p class="text-[11px] text-[#908fa0] mt-1 leading-relaxed">${esc(r.action)}</p>
484
489
  </div>
485
490
  </div>`;
486
491
  }).join('')}
@@ -619,7 +624,7 @@ ${anomalies.hasAnomalies ? `
619
624
  </thead>
620
625
  <tbody class="divide-y divide-[rgba(70,69,84,0.15)]">
621
626
  ${claudeMdStack.globalSections.map(s => `<tr class="tbl-row">
622
- <td class="px-8 py-3 text-sm text-[#e3e2e3]">${s.name}</td>
627
+ <td class="px-8 py-3 text-sm text-[#e3e2e3]">${esc(s.name)}</td>
623
628
  <td class="px-8 py-3 font-mono text-sm text-[#c7c4d7] text-right">${s.lines}</td>
624
629
  <td class="px-8 py-3 font-mono text-sm text-[#c7c4d7] text-right">${s.tokens.toLocaleString()}</td>
625
630
  </tr>`).join('')}
@@ -1066,25 +1071,44 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
1066
1071
  if(!sec || !stats.totalReports) return;
1067
1072
  sec.style.display = '';
1068
1073
 
1069
- // Count + percentile
1070
- var total = stats.totalReports || 0;
1071
- document.getElementById('community-count').textContent = total + ' users worldwide';
1074
+ // Re-grade function — applies current grading logic to stored ratios
1075
+ // so old telemetry entries get accurate grades, not the inflated v0.4 grades
1076
+ function regrade(ratio){
1077
+ // Simplified: ratio signal dominates (40%), assume stable trend (70), good hit rate (85), no breaks (100)
1078
+ var rs;
1079
+ if(ratio<=200)rs=100;else if(ratio<=350)rs=85;else if(ratio<=500)rs=70;
1080
+ else if(ratio<=650)rs=55;else if(ratio<=800)rs=42;else if(ratio<=1000)rs=30;
1081
+ else if(ratio<=1500)rs=18;else if(ratio<=2000)rs=10;else rs=3;
1082
+ var composite=Math.round(85*0.15+rs*0.40+70*0.30+100*0.15);
1083
+ if(rs<=5)composite=Math.min(composite,38);
1084
+ if(composite>=75)return'A';if(composite>=60)return'B';if(composite>=45)return'C';if(composite>=30)return'D';return'F';
1085
+ }
1086
+
1087
+ // Count + geographic spread (don't show raw user count — looks small early on)
1088
+ var countries = stats.countries ? Object.keys(stats.countries) : [];
1089
+ document.getElementById('community-count').textContent = countries.length + ' countries';
1072
1090
 
1073
1091
  // Calculate percentile from recent entries
1074
1092
  var recent = stats.recent || [];
1075
1093
  var ratios = recent.map(function(r){return r.ratio||9999}).sort(function(a,b){return a-b});
1076
1094
  var betterThan = ratios.filter(function(r){return r > MY_RATIO}).length;
1077
- var pctile = total > 0 ? Math.round(betterThan / ratios.length * 100) : 0;
1095
+ var pctile = ratios.length > 0 ? Math.round(betterThan / ratios.length * 100) : 0;
1078
1096
  document.getElementById('community-percentile').innerHTML =
1079
1097
  'Your cache ratio of <strong style="color:#e3e2e3">' + MY_RATIO + ':1</strong> is better than <strong style="color:#c0c1ff">' + pctile + '%</strong> of CC Hubber users';
1080
1098
 
1081
- // Grade distribution bar
1082
- var grades = stats.grades || {};
1083
- var gTotal = Object.values(grades).reduce(function(s,v){return s+v},0);
1099
+ // Grade distribution — use stored grade for v0.5+ entries, regrade older ones
1100
+ function getGrade(entry){
1101
+ var v = entry.v || entry.version || '0.3.3';
1102
+ var parts = v.split('.'); var isNew = (parseInt(parts[0]||0)>0) || (parseInt(parts[1]||0)>=5);
1103
+ return (isNew && entry.grade) ? entry.grade : (entry.ratio ? regrade(entry.ratio) : (entry.grade || 'C'));
1104
+ }
1105
+ var regraded = {A:0,B:0,C:0,D:0,F:0};
1106
+ recent.forEach(function(r){regraded[getGrade(r)]++});
1107
+ var gTotal = Object.values(regraded).reduce(function(s,v){return s+v},0);
1084
1108
  var bar = document.getElementById('grade-dist-bar');
1085
1109
  var labels = document.getElementById('grade-dist-labels');
1086
1110
  ['A','B','C','D','F'].forEach(function(g){
1087
- var count = grades[g] || 0;
1111
+ var count = regraded[g] || 0;
1088
1112
  var pct = gTotal > 0 ? (count/gTotal*100) : 0;
1089
1113
  if(pct > 0){
1090
1114
  var seg = document.createElement('div');
@@ -1098,22 +1122,18 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
1098
1122
  }
1099
1123
  });
1100
1124
 
1101
- // Leaderboard table
1125
+ // Leaderboard table — re-graded
1102
1126
  var tbody = document.getElementById('leaderboard-body');
1103
1127
  var sorted = recent.filter(function(r){return r.ratio}).sort(function(a,b){return (a.ratio||9999)-(b.ratio||9999)});
1104
- var myRank = -1;
1105
- sorted.forEach(function(entry, i){
1106
- if(myRank<0 && (entry.ratio||9999) >= MY_RATIO) myRank = i;
1107
- });
1108
- if(myRank<0) myRank = sorted.length;
1109
1128
 
1110
1129
  var html = '';
1111
1130
  sorted.forEach(function(entry, i){
1112
- var isMe = Math.abs((entry.ratio||0) - MY_RATIO) < 10 && entry.grade === MY_GRADE;
1131
+ var g = getGrade(entry);
1132
+ var isMe = Math.abs((entry.ratio||0) - MY_RATIO) < 50;
1113
1133
  var rowStyle = isMe ? 'background:rgba(192,193,255,0.06);border-left:2px solid #c0c1ff;' : '';
1114
1134
  html += '<tr style="border-bottom:1px solid rgba(70,69,84,0.1);'+rowStyle+'">';
1115
1135
  html += '<td class="px-8 py-3 text-sm font-mono text-[#908fa0]">#'+(i+1)+'</td>';
1116
- html += '<td class="px-4 py-3 text-sm font-bold text-center" style="color:'+gradeColors[entry.grade||'C']+'">'+entry.grade+'</td>';
1136
+ html += '<td class="px-4 py-3 text-sm font-bold text-center" style="color:'+gradeColors[g]+'">'+g+'</td>';
1117
1137
  html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.ratio||'?')+':1</td>';
1118
1138
  html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost||'?')+'</td>';
1119
1139
  html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus||'?')+'%</td>';