cchubber 0.3.9 → 0.4.1

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.3.9",
3
+ "version": "0.4.1",
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": {
package/src/cli/index.js CHANGED
@@ -164,8 +164,9 @@ async function main() {
164
164
 
165
165
  // Anonymous telemetry (opt out: --no-telemetry or CC_HUBBER_TELEMETRY=0)
166
166
  if (shouldSendTelemetry(flags)) {
167
- sendTelemetry(report);
168
- console.log(' ○ Anonymous stats shared (opt out: --no-telemetry)');
167
+ console.log(' ○ Sharing anonymous stats...');
168
+ await sendTelemetry(report);
169
+ console.log(' ✓ Stats shared (opt out: --no-telemetry)');
169
170
  }
170
171
 
171
172
  const outputPath = flags.output || join(process.cwd(), 'cchubber-report.html');
@@ -243,7 +243,7 @@ export function aggregateByProject(entries, claudeDir) {
243
243
  }
244
244
 
245
245
  // Decode project paths from directory names
246
- // Claude Code encodes paths as: C--Users-asmir-Documents-Project-Name
246
+ // Claude Code encodes paths as: C--Users-username-Documents-Project-Name
247
247
  // Decode: replace leading drive letter pattern, split on -, take last meaningful segments
248
248
  for (const proj of Object.values(byProject)) {
249
249
  proj.sessionCount = proj.sessions.size;
@@ -268,8 +268,8 @@ function cleanModelName(name) {
268
268
 
269
269
  /**
270
270
  * Decode Claude Code's encoded project directory name into a readable path and name.
271
- * Format: C--Users-asmir-Documents-Obsidian-Architect-OS-01-Projects-My-Project
272
- * Becomes: C:/Users/asmir/Documents/.../My-Project → name: "My-Project"
271
+ * Format: C--Users-username-Documents-Projects-My-Project
272
+ * Becomes: C:/Users/username/Documents/.../My-Project → name: "My-Project"
273
273
  */
274
274
  function decodeProjectHash(hash) {
275
275
  if (!hash || hash === 'unknown') return { path: null, name: 'Unknown' };
@@ -487,8 +487,9 @@ ${inflection && inflection.multiplier >= 1.5 ? `
487
487
  <!-- 7. PROJECTS TABLE -->
488
488
  ${projectBreakdown && projectBreakdown.length > 0 ? `
489
489
  <section class="bg-[#1b1c1d] rounded-xl border border-[rgba(70,69,84,0.15)] overflow-hidden">
490
- <div class="px-8 py-6 border-b border-[rgba(70,69,84,0.15)]">
490
+ <div class="px-8 py-6 border-b border-[rgba(70,69,84,0.15)] flex justify-between items-center">
491
491
  <h3 class="text-xl font-bold text-[#e3e2e3]">Projects</h3>
492
+ <button id="toggle-paths" onclick="document.querySelectorAll('.proj-name,.proj-path').forEach(e=>e.style.filter=e.style.filter?'':'blur(8px)');this.textContent=this.textContent==='Hide names'?'Show names':'Hide names'" class="text-[10px] font-mono text-[#908fa0] px-3 py-1 border border-[rgba(70,69,84,0.3)] rounded cursor-pointer hover:text-[#e3e2e3]">Hide names</button>
492
493
  </div>
493
494
  <div class="overflow-x-auto">
494
495
  <table class="w-full text-left" id="proj-tbl">
@@ -675,8 +676,8 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
675
676
  for(var i=0;i<P.length;i++){
676
677
  var p=P[i];
677
678
  h+='<tr class="tbl-row">';
678
- h+='<td class="px-8 py-4 text-sm font-semibold text-[#e3e2e3]">'+p.name;
679
- if(p.path)h+='<br><span class="text-[10px] text-[#908fa0] font-mono">'+p.path+'</span>';
679
+ h+='<td class="px-8 py-4 text-sm font-semibold text-[#e3e2e3]"><span class="proj-name">'+p.name+'</span>';
680
+ if(p.path)h+='<br><span class="proj-path text-[10px] text-[#908fa0] font-mono">'+p.path+'</span>';
680
681
  h+='</td>';
681
682
  h+='<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7]">'+p.messages.toLocaleString()+'</td>';
682
683
  h+='<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7]">'+p.sessions+'</td>';
package/src/telemetry.js CHANGED
@@ -136,24 +136,29 @@ export function sendTelemetry(report) {
136
136
  ...gatherEnvironmentData(),
137
137
  };
138
138
 
139
- // Fire and forget never blocks the CLI
140
- try {
141
- const data = JSON.stringify(payload);
142
- const url = new URL(TELEMETRY_URL);
143
- const req = https.request({
144
- hostname: url.hostname,
145
- path: url.pathname,
146
- method: 'POST',
147
- headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
148
- });
149
- req.on('error', () => {}); // silent fail
150
- req.setTimeout(3000, () => req.destroy());
151
- req.write(data);
152
- req.end();
153
- markTelemetrySent();
154
- } catch {
155
- // never crash on telemetry
156
- }
139
+ // Returns a promise that resolves when the request completes (or times out)
140
+ // CLI must await this before exiting, otherwise the process kills the request
141
+ return new Promise((resolve) => {
142
+ try {
143
+ const data = JSON.stringify(payload);
144
+ const url = new URL(TELEMETRY_URL);
145
+ const req = https.request({
146
+ hostname: url.hostname,
147
+ path: url.pathname,
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
150
+ }, (res) => {
151
+ res.resume(); // drain response
152
+ res.on('end', () => { markTelemetrySent(); resolve(); });
153
+ });
154
+ req.on('error', () => resolve()); // silent fail, still resolve
155
+ req.setTimeout(4000, () => { req.destroy(); resolve(); });
156
+ req.write(data);
157
+ req.end();
158
+ } catch {
159
+ resolve(); // never block on telemetry failure
160
+ }
161
+ });
157
162
  }
158
163
 
159
164
  function costBucket(cost) {