cchubber 0.5.2 → 0.5.4
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 +1 -1
- package/src/renderers/html-report.js +35 -7
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
|
@@ -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
|
|
@@ -1122,25 +1124,51 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
1122
1124
|
}
|
|
1123
1125
|
});
|
|
1124
1126
|
|
|
1125
|
-
// Leaderboard table — re-graded
|
|
1127
|
+
// Leaderboard table — re-graded, with user's position injected
|
|
1126
1128
|
var tbody = document.getElementById('leaderboard-body');
|
|
1127
1129
|
var sorted = recent.filter(function(r){return r.ratio}).sort(function(a,b){return (a.ratio||9999)-(b.ratio||9999)});
|
|
1128
1130
|
|
|
1131
|
+
// Find where the user would rank
|
|
1132
|
+
var myRank = sorted.findIndex(function(e){return (e.ratio||0) >= MY_RATIO});
|
|
1133
|
+
if(myRank < 0) myRank = sorted.length;
|
|
1134
|
+
|
|
1135
|
+
// Show top 10 + user's position (if not in top 10)
|
|
1136
|
+
var showIndices = [];
|
|
1137
|
+
for(var si=0;si<Math.min(10,sorted.length);si++) showIndices.push(si);
|
|
1138
|
+
var userInTop = myRank < 10;
|
|
1139
|
+
if(!userInTop && myRank < sorted.length){
|
|
1140
|
+
showIndices.push(-1); // separator
|
|
1141
|
+
if(myRank > 0) showIndices.push(myRank-1);
|
|
1142
|
+
showIndices.push(myRank);
|
|
1143
|
+
if(myRank+1 < sorted.length) showIndices.push(myRank+1);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1129
1146
|
var html = '';
|
|
1130
|
-
|
|
1147
|
+
for(var si2=0;si2<showIndices.length;si2++){
|
|
1148
|
+
var idx = showIndices[si2];
|
|
1149
|
+
if(idx === -1){
|
|
1150
|
+
html += '<tr><td colspan="6" class="px-8 py-1 text-center text-[10px] text-[#908fa0]">···</td></tr>';
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
var entry = sorted[idx];
|
|
1154
|
+
if(!entry) continue;
|
|
1131
1155
|
var g = getGrade(entry);
|
|
1132
|
-
var isMe =
|
|
1133
|
-
var rowStyle = isMe ? 'background:rgba(192,193,255,0.
|
|
1156
|
+
var isMe = idx === myRank;
|
|
1157
|
+
var rowStyle = isMe ? 'background:rgba(192,193,255,0.08);border-left:3px solid #c0c1ff;' : '';
|
|
1134
1158
|
html += '<tr style="border-bottom:1px solid rgba(70,69,84,0.1);'+rowStyle+'">';
|
|
1135
|
-
html += '<td class="px-8 py-3 text-sm font-mono text-[#908fa0]">#'+(
|
|
1159
|
+
html += '<td class="px-8 py-3 text-sm font-mono '+(isMe?'text-[#c0c1ff] font-bold':'text-[#908fa0]')+'">#'+(idx+1)+(isMe?' ← you':'')+'</td>';
|
|
1136
1160
|
html += '<td class="px-4 py-3 text-sm font-bold text-center" style="color:'+gradeColors[g]+'">'+g+'</td>';
|
|
1137
1161
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.ratio||'?')+':1</td>';
|
|
1138
1162
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#908fa0]">'+(entry.cost||'?')+'</td>';
|
|
1139
1163
|
html += '<td class="px-4 py-3 text-sm font-mono text-[#c7c4d7] text-right">'+(entry.opus||'?')+'%</td>';
|
|
1140
1164
|
html += '<td class="px-8 py-3 text-sm text-[#908fa0]">'+(entry.country||'?')+'</td>';
|
|
1141
1165
|
html += '</tr>';
|
|
1142
|
-
}
|
|
1166
|
+
}
|
|
1143
1167
|
tbody.innerHTML = html;
|
|
1168
|
+
|
|
1169
|
+
// Update percentile text with rank
|
|
1170
|
+
document.getElementById('community-percentile').innerHTML =
|
|
1171
|
+
"You are <strong style=\\"color:#c0c1ff\\">#"+(myRank+1)+" of "+sorted.length+"</strong> users by cache efficiency. Better than <strong style=\\"color:#c0c1ff\\">"+pctile+"%</strong>";
|
|
1144
1172
|
})(stats);
|
|
1145
1173
|
})();
|
|
1146
1174
|
</script>
|