cchubber 0.5.0 → 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.0",
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
 
@@ -467,21 +472,20 @@ ${inflection && inflection.multiplier >= 1.5 ? `
467
472
  <div class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
468
473
  <h3 class="text-xl font-bold text-[#e3e2e3] mb-6">Recommendations</h3>
469
474
  <div class="space-y-3">
470
- ${recommendations.map(r => {
475
+ ${recommendations.map((r, idx) => {
471
476
  const sev = sevColorMap[r.severity] || sevColorMap.info;
472
- const safeTitle = r.title.replace(/'/g, "\\'").replace(/"/g, '&quot;');
473
- const safeAction = r.action.replace(/'/g, "\\'").replace(/"/g, '&quot;');
474
- const clipboardText = `CC Hubber flagged this about my setup:\\n\\n${r.title}\\n\\n${r.action}\\n\\nBefore making changes:\\n1. Read the relevant files first to understand the current setup\\n2. Do NOT remove or modify anything without asking me — some things are there intentionally\\n3. Show me what each section/setting costs in tokens and let me decide what to keep\\n4. Suggest optimizations (move to skills, use hooks, restructure) rather than deletion\\n5. Do not break existing working functionality`;
477
+ const clipboardText = `CC Hubber flagged this about my setup:\n\n${r.title}\n\n${r.action}\n\nBefore making changes:\n1. Read the relevant files first to understand the current setup\n2. Do NOT remove or modify anything without asking me — some things are there intentionally\n3. Show me what each section/setting costs in tokens and let me decide what to keep\n4. Suggest optimizations (move to skills, use hooks, restructure) rather than deletion\n5. Do not break existing working functionality`;
478
+ const b64 = Buffer.from(clipboardText).toString('base64');
475
479
  return `<div class="p-4 bg-[#0d0e0f] rounded-r-lg flex items-start gap-4" style="border-left:3px solid ${sev.border}">
476
480
  <div class="flex-1 min-w-0">
477
481
  <div class="flex items-start justify-between gap-4">
478
- <p class="text-[13px] font-semibold text-[#e3e2e3]">${r.title}</p>
482
+ <p class="text-[13px] font-semibold text-[#e3e2e3]">${esc(r.title)}</p>
479
483
  <div class="flex items-center gap-2 shrink-0">
480
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>` : ''}
481
- ${r.severity !== 'positive' ? `<button onclick="navigator.clipboard.writeText('${clipboardText}');this.textContent='Copied!';setTimeout(()=>this.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>` : ''}
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>` : ''}
482
486
  </div>
483
487
  </div>
484
- <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>
485
489
  </div>
486
490
  </div>`;
487
491
  }).join('')}
@@ -620,7 +624,7 @@ ${anomalies.hasAnomalies ? `
620
624
  </thead>
621
625
  <tbody class="divide-y divide-[rgba(70,69,84,0.15)]">
622
626
  ${claudeMdStack.globalSections.map(s => `<tr class="tbl-row">
623
- <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>
624
628
  <td class="px-8 py-3 font-mono text-sm text-[#c7c4d7] text-right">${s.lines}</td>
625
629
  <td class="px-8 py-3 font-mono text-sm text-[#c7c4d7] text-right">${s.tokens.toLocaleString()}</td>
626
630
  </tr>`).join('')}
@@ -1067,25 +1071,44 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
1067
1071
  if(!sec || !stats.totalReports) return;
1068
1072
  sec.style.display = '';
1069
1073
 
1070
- // Count + percentile
1071
- var total = stats.totalReports || 0;
1072
- 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';
1073
1090
 
1074
1091
  // Calculate percentile from recent entries
1075
1092
  var recent = stats.recent || [];
1076
1093
  var ratios = recent.map(function(r){return r.ratio||9999}).sort(function(a,b){return a-b});
1077
1094
  var betterThan = ratios.filter(function(r){return r > MY_RATIO}).length;
1078
- var pctile = total > 0 ? Math.round(betterThan / ratios.length * 100) : 0;
1095
+ var pctile = ratios.length > 0 ? Math.round(betterThan / ratios.length * 100) : 0;
1079
1096
  document.getElementById('community-percentile').innerHTML =
1080
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';
1081
1098
 
1082
- // Grade distribution bar
1083
- var grades = stats.grades || {};
1084
- 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);
1085
1108
  var bar = document.getElementById('grade-dist-bar');
1086
1109
  var labels = document.getElementById('grade-dist-labels');
1087
1110
  ['A','B','C','D','F'].forEach(function(g){
1088
- var count = grades[g] || 0;
1111
+ var count = regraded[g] || 0;
1089
1112
  var pct = gTotal > 0 ? (count/gTotal*100) : 0;
1090
1113
  if(pct > 0){
1091
1114
  var seg = document.createElement('div');
@@ -1099,22 +1122,18 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
1099
1122
  }
1100
1123
  });
1101
1124
 
1102
- // Leaderboard table
1125
+ // Leaderboard table — re-graded
1103
1126
  var tbody = document.getElementById('leaderboard-body');
1104
1127
  var sorted = recent.filter(function(r){return r.ratio}).sort(function(a,b){return (a.ratio||9999)-(b.ratio||9999)});
1105
- var myRank = -1;
1106
- sorted.forEach(function(entry, i){
1107
- if(myRank<0 && (entry.ratio||9999) >= MY_RATIO) myRank = i;
1108
- });
1109
- if(myRank<0) myRank = sorted.length;
1110
1128
 
1111
1129
  var html = '';
1112
1130
  sorted.forEach(function(entry, i){
1113
- 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;
1114
1133
  var rowStyle = isMe ? 'background:rgba(192,193,255,0.06);border-left:2px solid #c0c1ff;' : '';
1115
1134
  html += '<tr style="border-bottom:1px solid rgba(70,69,84,0.1);'+rowStyle+'">';
1116
1135
  html += '<td class="px-8 py-3 text-sm font-mono text-[#908fa0]">#'+(i+1)+'</td>';
1117
- 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>';
1118
1137
  html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.ratio||'?')+':1</td>';
1119
1138
  html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost||'?')+'</td>';
1120
1139
  html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus||'?')+'%</td>';