cchubber 0.3.6 → 0.3.8
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/cli/index.js +9 -2
- package/src/renderers/html-report.js +3 -3
- package/src/telemetry.js +14 -9
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -4,6 +4,13 @@ import { resolve, join } from 'path';
|
|
|
4
4
|
import { existsSync, writeFileSync } from 'fs';
|
|
5
5
|
import { homedir, platform } from 'os';
|
|
6
6
|
import { exec } from 'child_process';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PKG = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
13
|
+
const VERSION = PKG.version;
|
|
7
14
|
|
|
8
15
|
import { readAllJSONL, aggregateDaily, aggregateByModel, aggregateByProject } from '../readers/jsonl-reader.js';
|
|
9
16
|
import { readStatsCache } from '../readers/stats-cache.js';
|
|
@@ -41,7 +48,7 @@ const flags = {
|
|
|
41
48
|
if (flags.help) {
|
|
42
49
|
console.log(`
|
|
43
50
|
╔═══════════════════════════════════════════════╗
|
|
44
|
-
║ CC Hubber
|
|
51
|
+
║ CC Hubber v${VERSION} ║
|
|
45
52
|
║ What you spent. Why you spent it. Is that ║
|
|
46
53
|
║ normal. ║
|
|
47
54
|
╚═══════════════════════════════════════════════╝
|
|
@@ -78,7 +85,7 @@ async function main() {
|
|
|
78
85
|
|
|
79
86
|
console.log(`
|
|
80
87
|
/\\ _ /\\
|
|
81
|
-
/ \\(_)/ \\ CC Hubber
|
|
88
|
+
/ \\(_)/ \\ CC Hubber v${VERSION}
|
|
82
89
|
\\ / ◉ \\ / What you spent. Why. Is that normal.
|
|
83
90
|
\\/ ~ \\/
|
|
84
91
|
`);
|
|
@@ -15,7 +15,7 @@ export function renderHTML(report) {
|
|
|
15
15
|
date: d.date, cost: d.cost, cacheOutputRatio: d.cacheOutputRatio || 0, isAnomaly: anomalyDates.has(d.date),
|
|
16
16
|
})));
|
|
17
17
|
|
|
18
|
-
const projectsJSON = JSON.stringify((projectBreakdown || []).
|
|
18
|
+
const projectsJSON = JSON.stringify((projectBreakdown || []).map(p => ({
|
|
19
19
|
name: p.name, path: p.path, messages: p.messageCount, sessions: p.sessionCount,
|
|
20
20
|
input: p.inputTokens, output: p.outputTokens, cacheRead: p.cacheReadTokens, cacheWrite: p.cacheCreationTokens,
|
|
21
21
|
})));
|
|
@@ -409,7 +409,7 @@ ${inflection && inflection.multiplier >= 1.5 ? `
|
|
|
409
409
|
<div>
|
|
410
410
|
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-4">Top Tools Usage</span>
|
|
411
411
|
<div class="space-y-3">
|
|
412
|
-
${sessionIntel.topTools.slice(0,
|
|
412
|
+
${sessionIntel.topTools.slice(0, 10).map((t, i) => `
|
|
413
413
|
<div class="space-y-1">
|
|
414
414
|
<div class="flex justify-between text-[11px] font-mono">
|
|
415
415
|
<span class="text-[#c7c4d7]">${t.name}</span>
|
|
@@ -672,7 +672,7 @@ ${cacheHealth.totalCacheBreaks > 0 ? `
|
|
|
672
672
|
if(ptb&&P.length>0){
|
|
673
673
|
P.sort(function(a,b){return(b.output/1e6*OUT+b.cacheRead/1e6*CACHE_R)-(a.output/1e6*OUT+a.cacheRead/1e6*CACHE_R)});
|
|
674
674
|
var h='';
|
|
675
|
-
for(var i=0;i<
|
|
675
|
+
for(var i=0;i<P.length;i++){
|
|
676
676
|
var p=P[i];
|
|
677
677
|
h+='<tr class="tbl-row">';
|
|
678
678
|
h+='<td class="px-8 py-4 text-sm font-semibold text-[#e3e2e3]">'+p.name;
|
package/src/telemetry.js
CHANGED
|
@@ -2,7 +2,12 @@ import https from 'https';
|
|
|
2
2
|
import { platform, arch, homedir, cpus, totalmem, freemem } from 'os';
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
5
|
+
import { execSync as rawExec } from 'child_process';
|
|
6
|
+
|
|
7
|
+
// Suppress stderr output on Windows (prevents "system cannot find path" spam)
|
|
8
|
+
function execSync(cmd, opts = {}) {
|
|
9
|
+
return rawExec(cmd, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'], ...opts });
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
// Anonymous usage telemetry — no PII, no tokens, no file contents.
|
|
8
13
|
// Opt out: npx cchubber --no-telemetry
|
|
@@ -364,14 +369,14 @@ function gatherEnvironmentData() {
|
|
|
364
369
|
|
|
365
370
|
// Git project signals (no URLs, no names — just metrics)
|
|
366
371
|
try {
|
|
367
|
-
data.gitCommitCount = parseInt(execSync('git rev-list --count HEAD 2>/dev/null', {
|
|
368
|
-
data.gitBranchCount = parseInt(execSync('git branch --list 2>/dev/null | wc -l', {
|
|
369
|
-
data.gitContributors = parseInt(execSync('git shortlog -sn --all 2>/dev/null | wc -l', {
|
|
370
|
-
const lastCommit = execSync('git log -1 --format=%ct 2>/dev/null', {
|
|
372
|
+
data.gitCommitCount = parseInt(execSync('git rev-list --count HEAD 2>/dev/null', {}).trim()) || 0;
|
|
373
|
+
data.gitBranchCount = parseInt(execSync('git branch --list 2>/dev/null | wc -l', {}).trim()) || 0;
|
|
374
|
+
data.gitContributors = parseInt(execSync('git shortlog -sn --all 2>/dev/null | wc -l', {}).trim()) || 0;
|
|
375
|
+
const lastCommit = execSync('git log -1 --format=%ct 2>/dev/null', {}).trim();
|
|
371
376
|
data.daysSinceLastCommit = lastCommit ? Math.round((Date.now()/1000 - parseInt(lastCommit)) / 86400) : null;
|
|
372
377
|
data.gitHost = (() => {
|
|
373
378
|
try {
|
|
374
|
-
const url = execSync('git remote get-url origin 2>/dev/null', {
|
|
379
|
+
const url = execSync('git remote get-url origin 2>/dev/null', {}).trim();
|
|
375
380
|
if (url.includes('github.com')) return 'github';
|
|
376
381
|
if (url.includes('gitlab')) return 'gitlab';
|
|
377
382
|
if (url.includes('bitbucket')) return 'bitbucket';
|
|
@@ -384,7 +389,7 @@ function gatherEnvironmentData() {
|
|
|
384
389
|
// File type distribution (language signals — count only, no names)
|
|
385
390
|
try {
|
|
386
391
|
const countExt = (ext) => {
|
|
387
|
-
try { return parseInt(execSync(`find . -maxdepth 4 -name "*.${ext}" -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" 2>/dev/null | wc -l`, {
|
|
392
|
+
try { return parseInt(execSync(`find . -maxdepth 4 -name "*.${ext}" -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" 2>/dev/null | wc -l`, {}).trim()) || 0; } catch { return 0; }
|
|
388
393
|
};
|
|
389
394
|
data.filesByType = {
|
|
390
395
|
js: countExt('js'), ts: countExt('ts'), tsx: countExt('tsx'), jsx: countExt('jsx'),
|
|
@@ -397,7 +402,7 @@ function gatherEnvironmentData() {
|
|
|
397
402
|
|
|
398
403
|
// JSONL total size (how much CC data they have)
|
|
399
404
|
try {
|
|
400
|
-
const totalJSONLSize = parseInt(execSync(`find "${join(claudeDir, 'projects')}" -name "*.jsonl" -not -path "*/subagents/*" 2>/dev/null -exec stat --format="%s" {} + 2>/dev/null | awk '{s+=$1}END{print s}'`, {
|
|
405
|
+
const totalJSONLSize = parseInt(execSync(`find "${join(claudeDir, 'projects')}" -name "*.jsonl" -not -path "*/subagents/*" 2>/dev/null -exec stat --format="%s" {} + 2>/dev/null | awk '{s+=$1}END{print s}'`, {}).trim()) || 0;
|
|
401
406
|
data.jsonlTotalMB = Math.round(totalJSONLSize / 1048576);
|
|
402
407
|
} catch { data.jsonlTotalMB = 0; }
|
|
403
408
|
|
|
@@ -499,7 +504,7 @@ function gatherEnvironmentData() {
|
|
|
499
504
|
}
|
|
500
505
|
|
|
501
506
|
// OS version
|
|
502
|
-
try { data.osVersion = execSync('ver 2>/dev/null || uname -r 2>/dev/null', {
|
|
507
|
+
try { data.osVersion = execSync('ver 2>/dev/null || uname -r 2>/dev/null', {}).trim().slice(0,50); } catch {}
|
|
503
508
|
|
|
504
509
|
// Workspace scale
|
|
505
510
|
try {
|