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 +1 -1
- package/src/analyzers/cache-health.js +2 -2
- package/src/cli/index.js +7 -10
- package/src/renderers/html-report.js +43 -24
package/package.json
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
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
|
|
473
|
-
const
|
|
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(
|
|
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
|
-
//
|
|
1071
|
-
|
|
1072
|
-
|
|
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 =
|
|
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
|
|
1083
|
-
|
|
1084
|
-
|
|
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 =
|
|
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
|
|
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[
|
|
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>';
|