cchubber 0.5.3 → 0.5.5
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 +30 -3
- package/package.json +4 -2
- package/src/renderers/html-report.js +18 -11
package/README.md
CHANGED
|
@@ -9,9 +9,7 @@ Your Claude Code usage, diagnosed. One command.
|
|
|
9
9
|
npx cchubber
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Reads your local data, generates an HTML report. No API keys, no accounts
|
|
13
|
-
|
|
14
|
-
Sends anonymous aggregate stats (grade, cache ratio, model split) once per day to help build community benchmarks. No tokens, no file contents, no project names. Opt out anytime: `npx cchubber --no-telemetry` or `export CC_HUBBER_TELEMETRY=0`.
|
|
12
|
+
Reads your local data, generates an HTML report. No API keys, no accounts. Sends anonymous stats for community benchmarks ([details](#telemetry)). Opt out: `--no-telemetry`.
|
|
15
13
|
|
|
16
14
|
Built during the March 2026 cache crisis because nobody could tell if they'd been hit. Thousands of users burning through limits 10-20x faster than normal, and Anthropic's only answer was "we're investigating." We wanted receipts.
|
|
17
15
|
|
|
@@ -103,6 +101,35 @@ Everything is local. CC Hubber reads files that already exist on your machine.
|
|
|
103
101
|
|
|
104
102
|
CC Hubber tells you why, and whether it's normal. Inflection detection, cache break estimation, model routing savings, session intelligence, trend-weighted grading. Different tools for different questions.
|
|
105
103
|
|
|
104
|
+
## Telemetry
|
|
105
|
+
|
|
106
|
+
CC Hubber sends anonymous usage stats once per day to power community benchmarks. This is what makes the leaderboard rank, grade calibration, and community comparisons in your recommendations work. Without it, every user gets the same generic thresholds instead of real benchmarks.
|
|
107
|
+
|
|
108
|
+
**What's collected:**
|
|
109
|
+
- Cache health: grade, ratio, hit rate, break count
|
|
110
|
+
- Cost: bucketed range (e.g. "$500-1K"), not exact amounts
|
|
111
|
+
- Models: opus/sonnet/haiku split percentages
|
|
112
|
+
- Sessions: count, average duration, tool usage counts
|
|
113
|
+
- Config: CLAUDE.md token count, hook count, MCP server names, skill count
|
|
114
|
+
- Environment: OS, architecture, Node version, Claude Code version
|
|
115
|
+
- Tech stack: boolean flags (uses React, TypeScript, Tailwind, etc.)
|
|
116
|
+
- Anonymous machine ID (random hash, not tied to any account)
|
|
117
|
+
|
|
118
|
+
**What's NOT collected:**
|
|
119
|
+
- No file contents, project names, or directory paths
|
|
120
|
+
- No API keys, tokens, or credentials
|
|
121
|
+
- No conversation text or prompts
|
|
122
|
+
- No personal information (name, email, IP address)
|
|
123
|
+
|
|
124
|
+
**Opt out anytime:**
|
|
125
|
+
```bash
|
|
126
|
+
npx cchubber --no-telemetry
|
|
127
|
+
# or permanently:
|
|
128
|
+
export CC_HUBBER_TELEMETRY=0
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Full source: [`src/telemetry.js`](src/telemetry.js)
|
|
132
|
+
|
|
106
133
|
## License
|
|
107
134
|
|
|
108
135
|
MIT
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cchubber",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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": {
|
|
7
7
|
"cchubber": "./src/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node src/cli/index.js"
|
|
10
|
+
"start": "node src/cli/index.js",
|
|
11
|
+
"test": "node test/smoke.js",
|
|
12
|
+
"release": "bash release.sh"
|
|
11
13
|
},
|
|
12
14
|
"keywords": [
|
|
13
15
|
"claude",
|
|
@@ -748,7 +748,9 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
748
748
|
|
|
749
749
|
function chart(d){
|
|
750
750
|
if(!svg)return;
|
|
751
|
-
if(!d.length){svg.innerHTML='<text x="450" y="100" text-anchor="middle" fill="#908fa0" font-size="13" font-family="Inter,sans-serif">No data</text>';return}
|
|
751
|
+
if(!d.length){svg.innerHTML='<text x="450" y="100" text-anchor="middle" fill="#908fa0" font-size="13" font-family="Inter,sans-serif">No data for this period</text>';return}
|
|
752
|
+
var total=d.reduce(function(s,x){return s+x.cost},0);
|
|
753
|
+
if(total<0.01){svg.innerHTML='<text x="450" y="100" text-anchor="middle" fill="#908fa0" font-size="13" font-family="Inter,sans-serif">No cost data — costs are only calculated from token counts, not the costUSD field</text>';return}
|
|
752
754
|
var mx=Math.max.apply(null,d.map(function(x){return x.cost}))*1.1;if(mx<0.01)mx=1;
|
|
753
755
|
var s='';
|
|
754
756
|
// grid lines
|
|
@@ -1062,6 +1064,7 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
1062
1064
|
// Community leaderboard — stats fetched at generation time, embedded as JSON
|
|
1063
1065
|
var MY_RATIO = ${cacheHealth.efficiencyRatio || 0};
|
|
1064
1066
|
var MY_GRADE = '${cacheHealth.grade?.letter || '?'}';
|
|
1067
|
+
var MY_COST = '${(() => { const c = totalCost; return c<10?'<10':c<50?'10-50':c<200?'50-200':c<500?'200-500':c<1000?'500-1K':c<5000?'1K-5K':'5K+'; })()}';
|
|
1065
1068
|
var gradeColors = {A:'#10b981',B:'#22d3ee',C:'#f59e0b',D:'#f97316',F:'#ef4444'};
|
|
1066
1069
|
var stats = ${JSON.stringify(report.communityStats || null)};
|
|
1067
1070
|
|
|
@@ -1123,20 +1126,24 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
1123
1126
|
});
|
|
1124
1127
|
|
|
1125
1128
|
// Leaderboard table — re-graded, with user's position injected
|
|
1129
|
+
// Filter: minimum activity to qualify (exclude <$10 and $10-50 users — too little data for meaningful grade)
|
|
1130
|
+
var validCosts = {'50-200':1,'200-500':1,'500-1K':1,'1K-5K':1,'5K+':1};
|
|
1126
1131
|
var tbody = document.getElementById('leaderboard-body');
|
|
1127
|
-
var sorted = recent.filter(function(r){return r.ratio}).sort(function(a,b){return (a.ratio||9999)-(b.ratio||9999)});
|
|
1132
|
+
var sorted = recent.filter(function(r){return r.ratio && (validCosts[r.cost] || r.cost===MY_COST)}).sort(function(a,b){return (a.ratio||9999)-(b.ratio||9999)});
|
|
1128
1133
|
|
|
1129
|
-
//
|
|
1134
|
+
// Insert user's own entry into the sorted list at the right position
|
|
1135
|
+
var myEntry = {ratio:MY_RATIO, grade:MY_GRADE, cost:MY_COST, opus:${report.modelRouting?.opusPct ?? 'null'}, country:'You', os:'${process.platform}', isMe:true};
|
|
1130
1136
|
var myRank = sorted.findIndex(function(e){return (e.ratio||0) >= MY_RATIO});
|
|
1131
1137
|
if(myRank < 0) myRank = sorted.length;
|
|
1138
|
+
sorted.splice(myRank, 0, myEntry);
|
|
1132
1139
|
|
|
1133
1140
|
// Show top 10 + user's position (if not in top 10)
|
|
1134
1141
|
var showIndices = [];
|
|
1135
1142
|
for(var si=0;si<Math.min(10,sorted.length);si++) showIndices.push(si);
|
|
1136
1143
|
var userInTop = myRank < 10;
|
|
1137
|
-
if(!userInTop
|
|
1144
|
+
if(!userInTop){
|
|
1138
1145
|
showIndices.push(-1); // separator
|
|
1139
|
-
if(myRank >
|
|
1146
|
+
if(myRank > 1) showIndices.push(myRank-1);
|
|
1140
1147
|
showIndices.push(myRank);
|
|
1141
1148
|
if(myRank+1 < sorted.length) showIndices.push(myRank+1);
|
|
1142
1149
|
}
|
|
@@ -1145,28 +1152,28 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
1145
1152
|
for(var si2=0;si2<showIndices.length;si2++){
|
|
1146
1153
|
var idx = showIndices[si2];
|
|
1147
1154
|
if(idx === -1){
|
|
1148
|
-
html += '<tr><td colspan="6" class="px-8 py-1 text-center text-[10px] text-[#908fa0]"
|
|
1155
|
+
html += '<tr><td colspan="6" class="px-8 py-1 text-center text-[10px] text-[#908fa0]">...</td></tr>';
|
|
1149
1156
|
continue;
|
|
1150
1157
|
}
|
|
1151
1158
|
var entry = sorted[idx];
|
|
1152
1159
|
if(!entry) continue;
|
|
1153
|
-
var g = getGrade(entry);
|
|
1154
|
-
var isMe =
|
|
1160
|
+
var g = entry.isMe ? MY_GRADE : getGrade(entry);
|
|
1161
|
+
var isMe = entry.isMe;
|
|
1155
1162
|
var rowStyle = isMe ? 'background:rgba(192,193,255,0.08);border-left:3px solid #c0c1ff;' : '';
|
|
1156
1163
|
html += '<tr style="border-bottom:1px solid rgba(70,69,84,0.1);'+rowStyle+'">';
|
|
1157
1164
|
html += '<td class="px-8 py-3 text-sm font-mono '+(isMe?'text-[#c0c1ff] font-bold':'text-[#908fa0]')+'">#'+(idx+1)+(isMe?' ← you':'')+'</td>';
|
|
1158
1165
|
html += '<td class="px-4 py-3 text-sm font-bold text-center" style="color:'+gradeColors[g]+'">'+g+'</td>';
|
|
1159
1166
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.ratio||'?')+':1</td>';
|
|
1160
1167
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost||'?')+'</td>';
|
|
1161
|
-
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus
|
|
1162
|
-
html += '<td class="px-8 py-3 text-sm text-[#908fa0]">'+(entry.country||'?')+'</td>';
|
|
1168
|
+
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus!=null?entry.opus:'?')+'%</td>';
|
|
1169
|
+
html += '<td class="px-8 py-3 text-sm text-[#908fa0]">'+(isMe?'You':(entry.country||'?'))+'</td>';
|
|
1163
1170
|
html += '</tr>';
|
|
1164
1171
|
}
|
|
1165
1172
|
tbody.innerHTML = html;
|
|
1166
1173
|
|
|
1167
1174
|
// Update percentile text with rank
|
|
1168
1175
|
document.getElementById('community-percentile').innerHTML =
|
|
1169
|
-
|
|
1176
|
+
"You are <strong style=\\"color:#c0c1ff\\">#"+(myRank+1)+" of "+(sorted.length)+"</strong> users by cache efficiency. Better than <strong style=\\"color:#c0c1ff\\">"+pctile+"%</strong>";
|
|
1170
1177
|
})(stats);
|
|
1171
1178
|
})();
|
|
1172
1179
|
</script>
|