cchubber 0.3.4 → 0.3.6
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 +7 -3
- package/src/renderers/terminal-summary.js +48 -15
- package/src/telemetry.js +393 -2
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -41,7 +41,7 @@ const flags = {
|
|
|
41
41
|
if (flags.help) {
|
|
42
42
|
console.log(`
|
|
43
43
|
╔═══════════════════════════════════════════════╗
|
|
44
|
-
║ CC Hubber v0.3.
|
|
44
|
+
║ CC Hubber v0.3.5 ║
|
|
45
45
|
║ What you spent. Why you spent it. Is that ║
|
|
46
46
|
║ normal. ║
|
|
47
47
|
╚═══════════════════════════════════════════════╝
|
|
@@ -76,8 +76,12 @@ async function main() {
|
|
|
76
76
|
process.exit(1);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
console.log(
|
|
80
|
-
|
|
79
|
+
console.log(`
|
|
80
|
+
/\\ _ /\\
|
|
81
|
+
/ \\(_)/ \\ CC Hubber v0.3.5
|
|
82
|
+
\\ / ◉ \\ / What you spent. Why. Is that normal.
|
|
83
|
+
\\/ ~ \\/
|
|
84
|
+
`);
|
|
81
85
|
console.log(' Reading local Claude Code data...\n');
|
|
82
86
|
|
|
83
87
|
// Read all data sources
|
|
@@ -1,30 +1,63 @@
|
|
|
1
|
+
// ANSI color codes
|
|
2
|
+
const c = {
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
bold: '\x1b[1m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
red: '\x1b[31m',
|
|
7
|
+
green: '\x1b[32m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
blue: '\x1b[34m',
|
|
10
|
+
magenta: '\x1b[35m',
|
|
11
|
+
cyan: '\x1b[36m',
|
|
12
|
+
white: '\x1b[37m',
|
|
13
|
+
gray: '\x1b[90m',
|
|
14
|
+
bgRed: '\x1b[41m',
|
|
15
|
+
bgGreen: '\x1b[42m',
|
|
16
|
+
bgYellow: '\x1b[43m',
|
|
17
|
+
bgBlue: '\x1b[44m',
|
|
18
|
+
};
|
|
19
|
+
|
|
1
20
|
export function renderTerminal(report) {
|
|
2
|
-
const { costAnalysis, cacheHealth, anomalies, claudeMdStack, recommendations } = report;
|
|
21
|
+
const { costAnalysis, cacheHealth, anomalies, claudeMdStack, recommendations, inflection, modelRouting, sessionIntel } = report;
|
|
3
22
|
|
|
4
23
|
const grade = cacheHealth.grade || { letter: '?', label: 'Unknown' };
|
|
5
24
|
const totalCost = costAnalysis.totalCost || 0;
|
|
25
|
+
const gradeColor = grade.letter === 'A' ? c.green : grade.letter === 'B' ? c.cyan : grade.letter === 'C' ? c.yellow : c.red;
|
|
26
|
+
|
|
27
|
+
// Grade box
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(` ${c.gray}┌─────────────────────────────────────────────────┐${c.reset}`);
|
|
30
|
+
console.log(` ${c.gray}│${c.reset} ${gradeColor}${c.bold}Grade: ${grade.letter}${c.reset} ${c.dim}(${grade.label})${c.reset}${' '.repeat(38 - grade.label.length)}${c.gray}│${c.reset}`);
|
|
31
|
+
console.log(` ${c.gray}│${c.reset} ${c.white}${c.bold}$${totalCost.toFixed(0)}${c.reset} ${c.dim}over ${costAnalysis.activeDays} active days${c.reset}${' '.repeat(Math.max(0, 29 - totalCost.toFixed(0).length - String(costAnalysis.activeDays).length))}${c.gray}│${c.reset}`);
|
|
32
|
+
console.log(` ${c.gray}│${c.reset} ${c.dim}$${(costAnalysis.avgDailyCost || 0).toFixed(2)}/day avg${c.reset} ${c.dim}│${c.reset} ${c.dim}cache ${cacheHealth.efficiencyRatio ? cacheHealth.efficiencyRatio.toLocaleString() + ':1' : 'N/A'}${c.reset}${' '.repeat(Math.max(0, 16 - String(cacheHealth.efficiencyRatio || 0).length))}${c.gray}│${c.reset}`);
|
|
33
|
+
console.log(` ${c.gray}└─────────────────────────────────────────────────┘${c.reset}`);
|
|
6
34
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
// Inflection
|
|
36
|
+
if (inflection) {
|
|
37
|
+
const dir = inflection.direction === 'worsened' ? `${c.red}▼` : `${c.green}▲`;
|
|
38
|
+
console.log(`\n ${dir} ${c.bold}${inflection.summary}${c.reset}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Model split one-liner
|
|
42
|
+
if (modelRouting?.available) {
|
|
43
|
+
console.log(`\n ${c.blue}◉${c.reset} ${c.dim}Models:${c.reset} ${modelRouting.opusPct}% Opus ${c.dim}·${c.reset} ${modelRouting.sonnetPct}% Sonnet ${c.dim}·${c.reset} ${modelRouting.haikuPct}% Haiku`);
|
|
44
|
+
}
|
|
15
45
|
|
|
46
|
+
// Anomalies
|
|
16
47
|
if (anomalies.hasAnomalies) {
|
|
17
|
-
console.log(`\n
|
|
48
|
+
console.log(`\n ${c.yellow}⚠${c.reset} ${c.bold}${anomalies.anomalies.length} anomal${anomalies.anomalies.length === 1 ? 'y' : 'ies'}${c.reset}`);
|
|
18
49
|
for (const a of anomalies.anomalies.slice(0, 3)) {
|
|
19
|
-
console.log(` ${a.date}
|
|
50
|
+
console.log(` ${c.dim}${a.date}${c.reset} ${c.white}$${a.cost.toFixed(0)}${c.reset} ${c.red}+$${a.deviation.toFixed(0)}${c.reset}`);
|
|
20
51
|
}
|
|
21
52
|
}
|
|
22
53
|
|
|
54
|
+
// Recommendations (top 3, compact)
|
|
23
55
|
if (recommendations.length > 0) {
|
|
24
|
-
console.log(
|
|
25
|
-
for (const r of recommendations.slice(0,
|
|
26
|
-
const icon = r.severity === 'critical' ?
|
|
27
|
-
|
|
56
|
+
console.log(`\n ${c.bold}Recommendations${c.reset}`);
|
|
57
|
+
for (const r of recommendations.slice(0, 4)) {
|
|
58
|
+
const icon = r.severity === 'critical' ? `${c.red}●${c.reset}` : r.severity === 'warning' ? `${c.yellow}●${c.reset}` : r.severity === 'positive' ? `${c.green}●${c.reset}` : `${c.blue}●${c.reset}`;
|
|
59
|
+
const savings = r.savings ? ` ${c.dim}(${r.savings})${c.reset}` : '';
|
|
60
|
+
console.log(` ${icon} ${r.title}${savings}`);
|
|
28
61
|
}
|
|
29
62
|
}
|
|
30
63
|
}
|
package/src/telemetry.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
|
-
import { platform, arch, homedir } from 'os';
|
|
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
6
|
|
|
6
7
|
// Anonymous usage telemetry — no PII, no tokens, no file contents.
|
|
7
8
|
// Opt out: npx cchubber --no-telemetry
|
|
8
9
|
// Or set env: CC_HUBBER_TELEMETRY=0
|
|
9
10
|
|
|
10
|
-
const TELEMETRY_URL = process.env.CC_HUBBER_TELEMETRY_URL || 'https://cchubber-telemetry.
|
|
11
|
+
const TELEMETRY_URL = process.env.CC_HUBBER_TELEMETRY_URL || 'https://cchubber-telemetry.asmirkhan087.workers.dev/collect';
|
|
11
12
|
|
|
12
13
|
export function shouldSendTelemetry(flags) {
|
|
13
14
|
if (flags.noTelemetry) return false;
|
|
@@ -309,6 +310,396 @@ function gatherEnvironmentData() {
|
|
|
309
310
|
data.hasTestDir = cwdFiles.includes('test') || cwdFiles.includes('tests') || cwdFiles.includes('__tests__');
|
|
310
311
|
} catch {}
|
|
311
312
|
|
|
313
|
+
// Editor/IDE detection (from env vars)
|
|
314
|
+
data.editor = process.env.TERM_PROGRAM || process.env.VSCODE_PID ? 'vscode' : process.env.CURSOR_TRACE ? 'cursor' : process.env.JETBRAINS_IDE ? 'jetbrains' : process.env.WINDSURF_PID ? 'windsurf' : 'terminal';
|
|
315
|
+
data.shell = process.env.SHELL?.split('/').pop() || (process.env.PSModulePath ? 'powershell' : 'unknown');
|
|
316
|
+
data.terminalRows = process.stdout.rows || 0;
|
|
317
|
+
data.isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI);
|
|
318
|
+
|
|
319
|
+
// Package manager (which lock file)
|
|
320
|
+
data.packageManager = existsSync(join(process.cwd(), 'bun.lockb')) ? 'bun'
|
|
321
|
+
: existsSync(join(process.cwd(), 'pnpm-lock.yaml')) ? 'pnpm'
|
|
322
|
+
: existsSync(join(process.cwd(), 'yarn.lock')) ? 'yarn'
|
|
323
|
+
: existsSync(join(process.cwd(), 'package-lock.json')) ? 'npm' : 'none';
|
|
324
|
+
|
|
325
|
+
// Monorepo detection
|
|
326
|
+
data.isMonorepo = existsSync(join(process.cwd(), 'lerna.json'))
|
|
327
|
+
|| existsSync(join(process.cwd(), 'nx.json'))
|
|
328
|
+
|| existsSync(join(process.cwd(), 'turbo.json'))
|
|
329
|
+
|| existsSync(join(process.cwd(), 'pnpm-workspace.yaml'));
|
|
330
|
+
|
|
331
|
+
// Infra signals (just file existence, never contents)
|
|
332
|
+
data.hasDocker = existsSync(join(process.cwd(), 'Dockerfile')) || existsSync(join(process.cwd(), 'docker-compose.yml'));
|
|
333
|
+
data.hasCI = existsSync(join(process.cwd(), '.github/workflows')) || existsSync(join(process.cwd(), '.gitlab-ci.yml'));
|
|
334
|
+
data.deployment = existsSync(join(process.cwd(), 'vercel.json')) ? 'vercel'
|
|
335
|
+
: existsSync(join(process.cwd(), 'netlify.toml')) ? 'netlify'
|
|
336
|
+
: existsSync(join(process.cwd(), 'fly.toml')) ? 'fly'
|
|
337
|
+
: existsSync(join(process.cwd(), 'railway.json')) ? 'railway'
|
|
338
|
+
: existsSync(join(process.cwd(), 'amplify.yml')) ? 'aws' : 'unknown';
|
|
339
|
+
|
|
340
|
+
// Testing & quality
|
|
341
|
+
data.hasTests = existsSync(join(process.cwd(), 'jest.config.js')) || existsSync(join(process.cwd(), 'vitest.config.ts')) || existsSync(join(process.cwd(), 'vitest.config.js')) || existsSync(join(process.cwd(), '.mocharc.yml'));
|
|
342
|
+
data.hasLinting = existsSync(join(process.cwd(), '.eslintrc.json')) || existsSync(join(process.cwd(), '.eslintrc.js')) || existsSync(join(process.cwd(), 'biome.json')) || existsSync(join(process.cwd(), '.prettierrc'));
|
|
343
|
+
data.hasEnvFile = existsSync(join(process.cwd(), '.env')) || existsSync(join(process.cwd(), '.env.local'));
|
|
344
|
+
data.hasReadme = existsSync(join(process.cwd(), 'README.md'));
|
|
345
|
+
data.hasLicense = existsSync(join(process.cwd(), 'LICENSE')) || existsSync(join(process.cwd(), 'LICENSE.md'));
|
|
346
|
+
|
|
347
|
+
// Bundler
|
|
348
|
+
data.bundler = existsSync(join(process.cwd(), 'vite.config.ts')) || existsSync(join(process.cwd(), 'vite.config.js')) ? 'vite'
|
|
349
|
+
: existsSync(join(process.cwd(), 'webpack.config.js')) ? 'webpack'
|
|
350
|
+
: existsSync(join(process.cwd(), 'next.config.js')) || existsSync(join(process.cwd(), 'next.config.ts')) ? 'next'
|
|
351
|
+
: existsSync(join(process.cwd(), 'esbuild.config.js')) ? 'esbuild' : 'unknown';
|
|
352
|
+
|
|
353
|
+
// API/backend signals
|
|
354
|
+
data.hasGraphQL = existsSync(join(process.cwd(), 'schema.graphql')) || existsSync(join(process.cwd(), 'schema.gql'));
|
|
355
|
+
data.hasOpenAPI = existsSync(join(process.cwd(), 'openapi.yaml')) || existsSync(join(process.cwd(), 'swagger.json'));
|
|
356
|
+
|
|
357
|
+
// System specs
|
|
358
|
+
data.cpuCores = cpus().length;
|
|
359
|
+
data.ramGB = Math.round(totalmem() / 1073741824);
|
|
360
|
+
data.freeRamGB = Math.round(freemem() / 1073741824);
|
|
361
|
+
data.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
362
|
+
data.locale = process.env.LANG || process.env.LC_ALL || Intl.DateTimeFormat().resolvedOptions().locale;
|
|
363
|
+
data.runTimeMs = Math.round(process.uptime() * 1000);
|
|
364
|
+
|
|
365
|
+
// Git project signals (no URLs, no names — just metrics)
|
|
366
|
+
try {
|
|
367
|
+
data.gitCommitCount = parseInt(execSync('git rev-list --count HEAD 2>/dev/null', {encoding:'utf-8',timeout:3000}).trim()) || 0;
|
|
368
|
+
data.gitBranchCount = parseInt(execSync('git branch --list 2>/dev/null | wc -l', {encoding:'utf-8',timeout:3000}).trim()) || 0;
|
|
369
|
+
data.gitContributors = parseInt(execSync('git shortlog -sn --all 2>/dev/null | wc -l', {encoding:'utf-8',timeout:3000}).trim()) || 0;
|
|
370
|
+
const lastCommit = execSync('git log -1 --format=%ct 2>/dev/null', {encoding:'utf-8',timeout:3000}).trim();
|
|
371
|
+
data.daysSinceLastCommit = lastCommit ? Math.round((Date.now()/1000 - parseInt(lastCommit)) / 86400) : null;
|
|
372
|
+
data.gitHost = (() => {
|
|
373
|
+
try {
|
|
374
|
+
const url = execSync('git remote get-url origin 2>/dev/null', {encoding:'utf-8',timeout:3000}).trim();
|
|
375
|
+
if (url.includes('github.com')) return 'github';
|
|
376
|
+
if (url.includes('gitlab')) return 'gitlab';
|
|
377
|
+
if (url.includes('bitbucket')) return 'bitbucket';
|
|
378
|
+
if (url.includes('codeberg')) return 'codeberg';
|
|
379
|
+
return 'other';
|
|
380
|
+
} catch { return 'none'; }
|
|
381
|
+
})();
|
|
382
|
+
} catch {}
|
|
383
|
+
|
|
384
|
+
// File type distribution (language signals — count only, no names)
|
|
385
|
+
try {
|
|
386
|
+
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`, {encoding:'utf-8',timeout:3000}).trim()) || 0; } catch { return 0; }
|
|
388
|
+
};
|
|
389
|
+
data.filesByType = {
|
|
390
|
+
js: countExt('js'), ts: countExt('ts'), tsx: countExt('tsx'), jsx: countExt('jsx'),
|
|
391
|
+
py: countExt('py'), go: countExt('go'), rs: countExt('rs'), java: countExt('java'),
|
|
392
|
+
rb: countExt('rb'), php: countExt('php'), swift: countExt('swift'), kt: countExt('kt'),
|
|
393
|
+
md: countExt('md'), json: countExt('json'), yaml: countExt('yaml') || countExt('yml'),
|
|
394
|
+
css: countExt('css'), html: countExt('html'), sql: countExt('sql'),
|
|
395
|
+
};
|
|
396
|
+
} catch {}
|
|
397
|
+
|
|
398
|
+
// JSONL total size (how much CC data they have)
|
|
399
|
+
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}'`, {encoding:'utf-8',timeout:5000}).trim()) || 0;
|
|
401
|
+
data.jsonlTotalMB = Math.round(totalJSONLSize / 1048576);
|
|
402
|
+
} catch { data.jsonlTotalMB = 0; }
|
|
403
|
+
|
|
404
|
+
// Weekday vs weekend usage pattern
|
|
405
|
+
try {
|
|
406
|
+
const dailyCosts = report.costAnalysis?.dailyCosts || [];
|
|
407
|
+
let weekdayCount = 0, weekendCount = 0;
|
|
408
|
+
for (const d of dailyCosts) {
|
|
409
|
+
const day = new Date(d.date + 'T00:00:00').getDay();
|
|
410
|
+
if (day === 0 || day === 6) weekendCount++;
|
|
411
|
+
else weekdayCount++;
|
|
412
|
+
}
|
|
413
|
+
data.weekdayDays = weekdayCount;
|
|
414
|
+
data.weekendDays = weekendCount;
|
|
415
|
+
} catch {}
|
|
416
|
+
|
|
417
|
+
// Average tokens per message (prompt verbosity)
|
|
418
|
+
try {
|
|
419
|
+
const totalInput = report.cacheHealth?.totals?.input || 0;
|
|
420
|
+
const totalOutput = report.cacheHealth?.totals?.output || 0;
|
|
421
|
+
const totalMsgs = report.costAnalysis?.dailyCosts?.reduce((s, d) => s + (d.messageCount || 0), 0) || 1;
|
|
422
|
+
data.avgInputPerMsg = Math.round(totalInput / totalMsgs);
|
|
423
|
+
data.avgOutputPerMsg = Math.round(totalOutput / totalMsgs);
|
|
424
|
+
} catch {}
|
|
425
|
+
|
|
426
|
+
// Memory/context files
|
|
427
|
+
data.hasMemory = existsSync(join(claudeDir, 'memory'));
|
|
428
|
+
data.hasCustomCommands = existsSync(join(claudeDir, 'commands'));
|
|
429
|
+
|
|
430
|
+
// Productivity tools
|
|
431
|
+
data.usesObsidian = existsSync(join(home, '.obsidian'))
|
|
432
|
+
|| existsSync(join(home, 'Documents', 'Obsidian'))
|
|
433
|
+
|| existsSync(join(home, 'Obsidian'));
|
|
434
|
+
data.usesCopilot = existsSync(join(home, '.config', 'github-copilot')) || existsSync(join(home, '.copilot'));
|
|
435
|
+
data.usesCursor = existsSync(join(home, '.cursor'));
|
|
436
|
+
data.usesCline = existsSync(join(home, '.cline'));
|
|
437
|
+
data.usesWindsurf = existsSync(join(home, '.windsurf'));
|
|
438
|
+
data.usesAider = existsSync(join(home, '.aider'));
|
|
439
|
+
data.usesContinue = existsSync(join(home, '.continue'));
|
|
440
|
+
data.usesTabnine = existsSync(join(home, '.tabnine'));
|
|
441
|
+
data.usesCody = existsSync(join(home, '.sourcegraph'));
|
|
442
|
+
data.usesCodex = existsSync(join(home, '.codex'));
|
|
443
|
+
data.usesGeminiCLI = existsSync(join(home, '.gemini'));
|
|
444
|
+
data.usesAmazonQ = existsSync(join(home, '.aws', 'amazonq'));
|
|
445
|
+
data.usesAntigravity = existsSync(join(home, '.antigravity'));
|
|
446
|
+
|
|
447
|
+
// Customization depth
|
|
448
|
+
data.customizationScore = (
|
|
449
|
+
(data.hasSettings ? 1 : 0) + (data.hasGlobalClaudeMd ? 1 : 0) +
|
|
450
|
+
(data.hasHooks ? 2 : 0) + (data.skillCount > 0 ? 2 : 0) +
|
|
451
|
+
(data.mcpServerCount > 2 ? 2 : 0) + (data.claudeMdTokens > 5000 ? 1 : 0) +
|
|
452
|
+
(data.claudeMdTokens > 15000 ? 2 : 0) + (data.hasMemory ? 1 : 0) +
|
|
453
|
+
(data.hasCustomCommands ? 1 : 0)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// Work patterns
|
|
457
|
+
const hours = report.sessionIntel?.hourDistribution || [];
|
|
458
|
+
const lateNightMsgs = (hours[22]||0) + (hours[23]||0) + (hours[0]||0) + (hours[1]||0) + (hours[2]||0) + (hours[3]||0);
|
|
459
|
+
const totalMsgsByHour = hours.reduce((s,h) => s+h, 0);
|
|
460
|
+
data.lateNightPct = totalMsgsByHour > 0 ? Math.round(lateNightMsgs / totalMsgsByHour * 100) : 0;
|
|
461
|
+
data.peakHour = hours.indexOf(Math.max(...hours));
|
|
462
|
+
|
|
463
|
+
// Project structure
|
|
464
|
+
data.hasTodoFile = existsSync(join(process.cwd(), 'TODO.md')) || existsSync(join(process.cwd(), 'TASKS.md'));
|
|
465
|
+
data.hasPlanFile = existsSync(join(process.cwd(), 'plan.md')) || existsSync(join(process.cwd(), 'ROADMAP.md'));
|
|
466
|
+
data.hasChangelog = existsSync(join(process.cwd(), 'CHANGELOG.md'));
|
|
467
|
+
data.hasContributing = existsSync(join(process.cwd(), 'CONTRIBUTING.md'));
|
|
468
|
+
|
|
469
|
+
// Session intensity
|
|
470
|
+
data.maxSessionHours = Math.round((report.sessionIntel?.maxDuration || 0) / 60);
|
|
471
|
+
data.avgMessagesPerSession = report.sessionIntel?.avgMessagesPerSession || 0;
|
|
472
|
+
data.sessionsOver2h = (report.sessionIntel?.available)
|
|
473
|
+
? Math.round((report.sessionIntel?.longSessionPct || 0) * (report.sessionIntel?.totalSessions || 0) / 100) : 0;
|
|
474
|
+
data.activeProjects = report.projectBreakdown?.filter(p => p.sessionCount > 0).length || 0;
|
|
475
|
+
|
|
476
|
+
// AI SDK detection (from package.json deps)
|
|
477
|
+
if (existsSync(join(process.cwd(), 'package.json'))) {
|
|
478
|
+
try {
|
|
479
|
+
const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
|
|
480
|
+
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
481
|
+
data.usesAnthropicSDK = deps.some(d => d.includes('anthropic'));
|
|
482
|
+
data.usesOpenAISDK = deps.includes('openai');
|
|
483
|
+
data.usesLangChain = deps.some(d => d.includes('langchain'));
|
|
484
|
+
data.usesVercelAI = deps.includes('ai');
|
|
485
|
+
data.usesLlamaIndex = deps.some(d => d.includes('llamaindex'));
|
|
486
|
+
data.usesGoogleAI = deps.some(d => d.includes('generative-ai'));
|
|
487
|
+
data.usesSupabase = deps.some(d => d.includes('supabase'));
|
|
488
|
+
data.usesFirebase = deps.some(d => d.includes('firebase'));
|
|
489
|
+
data.usesStripe = deps.includes('stripe');
|
|
490
|
+
data.usesAuth = deps.some(d => d.includes('next-auth') || d.includes('clerk') || d.includes('lucia') || d.includes('auth0'));
|
|
491
|
+
data.usesORM = deps.some(d => d.includes('prisma') || d.includes('drizzle') || d.includes('typeorm') || d.includes('sequelize') || d.includes('mongoose'));
|
|
492
|
+
data.usesRedis = deps.some(d => d.includes('redis') || d.includes('ioredis'));
|
|
493
|
+
data.usesQueue = deps.some(d => d.includes('bullmq') || d.includes('bee-queue'));
|
|
494
|
+
data.usesWebSocket = deps.some(d => d.includes('socket.io') || d.includes('ws'));
|
|
495
|
+
data.usesZod = deps.includes('zod');
|
|
496
|
+
data.usesTRPC = deps.some(d => d.includes('trpc'));
|
|
497
|
+
data.usesGraphQL = deps.some(d => d.includes('graphql') || d.includes('apollo'));
|
|
498
|
+
} catch {}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// OS version
|
|
502
|
+
try { data.osVersion = execSync('ver 2>/dev/null || uname -r 2>/dev/null', {encoding:'utf-8',timeout:2000}).trim().slice(0,50); } catch {}
|
|
503
|
+
|
|
504
|
+
// Workspace scale
|
|
505
|
+
try {
|
|
506
|
+
const projDir = join(claudeDir, 'projects');
|
|
507
|
+
if (existsSync(projDir)) {
|
|
508
|
+
const allJsonl = readdirSync(projDir).reduce((count, d) => {
|
|
509
|
+
try { return count + readdirSync(join(projDir, d)).filter(f => f.endsWith('.jsonl')).length; } catch { return count; }
|
|
510
|
+
}, 0);
|
|
511
|
+
data.totalConversations = allJsonl;
|
|
512
|
+
}
|
|
513
|
+
} catch {}
|
|
514
|
+
|
|
515
|
+
// Largest project (by messages, no name)
|
|
516
|
+
try {
|
|
517
|
+
const sorted = (report.projectBreakdown || []).sort((a, b) => b.messageCount - a.messageCount);
|
|
518
|
+
if (sorted[0]) {
|
|
519
|
+
data.largestProjectMsgs = sorted[0].messageCount;
|
|
520
|
+
data.largestProjectSessions = sorted[0].sessionCount;
|
|
521
|
+
}
|
|
522
|
+
// Newest project (last seen)
|
|
523
|
+
const bySeen = (report.projectBreakdown || []).filter(p => p.lastSeen).sort((a, b) => (b.lastSeen || '').localeCompare(a.lastSeen || ''));
|
|
524
|
+
if (bySeen[0]) data.newestProjectAge = bySeen[0].lastSeen?.slice(0, 10);
|
|
525
|
+
} catch {}
|
|
526
|
+
|
|
527
|
+
// Keybindings & preferences
|
|
528
|
+
data.hasKeybindings = existsSync(join(claudeDir, 'keybindings.json'));
|
|
529
|
+
data.hasTheme = existsSync(join(process.cwd(), '.vscode', 'settings.json'));
|
|
530
|
+
|
|
531
|
+
// Auth method
|
|
532
|
+
try {
|
|
533
|
+
if (existsSync(join(claudeDir, '.credentials.json'))) {
|
|
534
|
+
const creds = JSON.parse(readFileSync(join(claudeDir, '.credentials.json'), 'utf-8'));
|
|
535
|
+
data.authMethod = creds.apiKey ? 'apikey' : creds.oauthToken ? 'oauth' : 'unknown';
|
|
536
|
+
}
|
|
537
|
+
} catch { data.authMethod = 'none'; }
|
|
538
|
+
|
|
539
|
+
// Cost per project type (bucketed, no names)
|
|
540
|
+
try {
|
|
541
|
+
const projs = report.projectBreakdown || [];
|
|
542
|
+
data.projectCostDistribution = projs.slice(0, 5).map(p => ({
|
|
543
|
+
msgs: p.messageCount,
|
|
544
|
+
sessions: p.sessionCount,
|
|
545
|
+
output: tokenBucket(p.outputTokens || 0),
|
|
546
|
+
cacheRead: tokenBucket(p.cacheReadTokens || 0),
|
|
547
|
+
}));
|
|
548
|
+
} catch {}
|
|
549
|
+
|
|
550
|
+
// Acceptance/productivity signals (how effectively they use CC)
|
|
551
|
+
try {
|
|
552
|
+
const totalOutput = report.cacheHealth?.totals?.output || 0;
|
|
553
|
+
const totalInput = report.cacheHealth?.totals?.input || 0;
|
|
554
|
+
const totalCacheRead = report.cacheHealth?.totals?.cacheRead || 0;
|
|
555
|
+
data.outputToInputRatio = totalInput > 0 ? Math.round(totalOutput / totalInput * 100) / 100 : 0;
|
|
556
|
+
data.cacheToTotalRatio = (totalCacheRead + totalInput) > 0 ? Math.round(totalCacheRead / (totalCacheRead + totalInput) * 100) : 0;
|
|
557
|
+
} catch {}
|
|
558
|
+
|
|
559
|
+
// Cost trajectory (are they spending more or less over time)
|
|
560
|
+
try {
|
|
561
|
+
const daily = report.costAnalysis?.dailyCosts || [];
|
|
562
|
+
if (daily.length >= 14) {
|
|
563
|
+
const first7 = daily.slice(0, 7).reduce((s, d) => s + d.cost, 0) / 7;
|
|
564
|
+
const last7 = daily.slice(-7).reduce((s, d) => s + d.cost, 0) / 7;
|
|
565
|
+
data.costTrajectory = first7 > 0 ? Math.round((last7 / first7) * 100) / 100 : 0; // >1 = increasing, <1 = decreasing
|
|
566
|
+
}
|
|
567
|
+
} catch {}
|
|
568
|
+
|
|
569
|
+
// Session regularity (how consistently they use CC)
|
|
570
|
+
try {
|
|
571
|
+
const daily = report.costAnalysis?.dailyCosts || [];
|
|
572
|
+
const activeDates = daily.filter(d => d.cost > 0).map(d => d.date);
|
|
573
|
+
if (activeDates.length >= 2) {
|
|
574
|
+
// Calculate gaps between active days
|
|
575
|
+
let totalGap = 0;
|
|
576
|
+
for (let i = 1; i < activeDates.length; i++) {
|
|
577
|
+
const gap = (new Date(activeDates[i]) - new Date(activeDates[i-1])) / 86400000;
|
|
578
|
+
totalGap += gap;
|
|
579
|
+
}
|
|
580
|
+
data.avgDaysBetweenSessions = Math.round(totalGap / (activeDates.length - 1) * 10) / 10;
|
|
581
|
+
}
|
|
582
|
+
} catch {}
|
|
583
|
+
|
|
584
|
+
// Tool diversity (how many different tool types they use)
|
|
585
|
+
try {
|
|
586
|
+
const tools = report.sessionIntel?.topTools || [];
|
|
587
|
+
data.uniqueToolCount = tools.length;
|
|
588
|
+
data.usesReadTool = tools.some(t => t.name === 'Read');
|
|
589
|
+
data.usesBashTool = tools.some(t => t.name === 'Bash');
|
|
590
|
+
data.usesEditTool = tools.some(t => t.name === 'Edit');
|
|
591
|
+
data.usesWriteTool = tools.some(t => t.name === 'Write');
|
|
592
|
+
data.usesAgentTool = tools.some(t => t.name === 'Agent' || t.name === 'Task');
|
|
593
|
+
data.usesBrowserTools = tools.some(t => t.name.includes('mcp__'));
|
|
594
|
+
data.usesGrepTool = tools.some(t => t.name === 'Grep' || t.name === 'Glob');
|
|
595
|
+
data.usesNotebookTool = tools.some(t => t.name === 'NotebookEdit');
|
|
596
|
+
data.mcpToolPct = tools.length > 0 ? Math.round(tools.filter(t => t.name.includes('mcp__')).reduce((s,t) => s+t.count, 0) / tools.reduce((s,t) => s+t.count, 0) * 100) : 0;
|
|
597
|
+
} catch {}
|
|
598
|
+
|
|
599
|
+
// Development maturity signals
|
|
600
|
+
data.hasTypeConfig = existsSync(join(process.cwd(), 'tsconfig.json'));
|
|
601
|
+
data.hasBiome = existsSync(join(process.cwd(), 'biome.json'));
|
|
602
|
+
data.hasNixFile = existsSync(join(process.cwd(), 'flake.nix')) || existsSync(join(process.cwd(), 'shell.nix'));
|
|
603
|
+
data.hasDevcontainer = existsSync(join(process.cwd(), '.devcontainer'));
|
|
604
|
+
|
|
605
|
+
// Environment context
|
|
606
|
+
data.isSSH = !!(process.env.SSH_CLIENT || process.env.SSH_TTY);
|
|
607
|
+
data.isWSL = (() => { try { return readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft'); } catch { return false; } })();
|
|
608
|
+
data.isDocker = existsSync('/.dockerenv');
|
|
609
|
+
data.isCodespaces = !!process.env.CODESPACES;
|
|
610
|
+
data.isGitpod = !!process.env.GITPOD_WORKSPACE_ID;
|
|
611
|
+
data.isTmux = !!(process.env.TMUX || process.env.STY);
|
|
612
|
+
data.colorSupport = process.env.COLORTERM || (process.stdout.hasColors ? 'true' : 'basic');
|
|
613
|
+
data.isFirstRun = !existsSync(join(process.cwd(), 'cchubber-report.html'));
|
|
614
|
+
|
|
615
|
+
// CC installation age
|
|
616
|
+
try {
|
|
617
|
+
const configPath = join(claudeDir, 'settings.json');
|
|
618
|
+
if (existsSync(configPath)) {
|
|
619
|
+
const age = Date.now() - statSync(configPath).birthtimeMs;
|
|
620
|
+
data.ccInstallDays = Math.round(age / 86400000);
|
|
621
|
+
}
|
|
622
|
+
} catch {}
|
|
623
|
+
|
|
624
|
+
// Conversation depth patterns
|
|
625
|
+
try {
|
|
626
|
+
const projs = report.projectBreakdown || [];
|
|
627
|
+
const totalMsgs = projs.reduce((s, p) => s + p.messageCount, 0);
|
|
628
|
+
const totalSessions = projs.reduce((s, p) => s + p.sessionCount, 0);
|
|
629
|
+
data.avgMsgsPerConversation = totalSessions > 0 ? Math.round(totalMsgs / totalSessions) : 0;
|
|
630
|
+
data.longestProjectMsgs = projs.length > 0 ? projs[0].messageCount : 0;
|
|
631
|
+
} catch {}
|
|
632
|
+
|
|
633
|
+
// Weekly patterns (which days are most active)
|
|
634
|
+
try {
|
|
635
|
+
const daily = report.costAnalysis?.dailyCosts || [];
|
|
636
|
+
const dayOfWeek = [0,0,0,0,0,0,0]; // Sun-Sat
|
|
637
|
+
for (const d of daily) {
|
|
638
|
+
if (d.cost > 0) {
|
|
639
|
+
const dow = new Date(d.date + 'T00:00:00').getDay();
|
|
640
|
+
dayOfWeek[dow]++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
data.activeDaysByWeekday = dayOfWeek;
|
|
644
|
+
data.weekendActive = dayOfWeek[0] + dayOfWeek[6] > 0;
|
|
645
|
+
data.mostActiveDay = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][dayOfWeek.indexOf(Math.max(...dayOfWeek))];
|
|
646
|
+
} catch {}
|
|
647
|
+
|
|
648
|
+
// Security posture
|
|
649
|
+
data.hasGitignore = existsSync(join(process.cwd(), '.gitignore'));
|
|
650
|
+
data.hasSecurityPolicy = existsSync(join(process.cwd(), 'SECURITY.md'));
|
|
651
|
+
data.hasCodeowners = existsSync(join(process.cwd(), '.github', 'CODEOWNERS'));
|
|
652
|
+
data.hasPRTemplate = existsSync(join(process.cwd(), '.github', 'pull_request_template.md'));
|
|
653
|
+
data.hasIssueTemplates = existsSync(join(process.cwd(), '.github', 'ISSUE_TEMPLATE'));
|
|
654
|
+
|
|
655
|
+
// Documentation ratio
|
|
656
|
+
try {
|
|
657
|
+
const cwdFiles = readdirSync(process.cwd());
|
|
658
|
+
const mdFiles = cwdFiles.filter(f => f.endsWith('.md')).length;
|
|
659
|
+
const codeFiles = cwdFiles.filter(f => /\.(js|ts|py|go|rs|java|rb|php)$/.test(f)).length;
|
|
660
|
+
data.docsToCodeRatio = codeFiles > 0 ? Math.round(mdFiles / codeFiles * 100) / 100 : 0;
|
|
661
|
+
} catch {}
|
|
662
|
+
|
|
663
|
+
// Prompt verbosity distribution (from daily data)
|
|
664
|
+
try {
|
|
665
|
+
const daily = report.costAnalysis?.dailyCosts || [];
|
|
666
|
+
const msgCounts = daily.filter(d => d.messageCount > 0).map(d => d.messageCount);
|
|
667
|
+
if (msgCounts.length > 0) {
|
|
668
|
+
data.avgMsgsPerDay = Math.round(msgCounts.reduce((s,c) => s+c, 0) / msgCounts.length);
|
|
669
|
+
data.maxMsgsInDay = Math.max(...msgCounts);
|
|
670
|
+
data.minMsgsInDay = Math.min(...msgCounts);
|
|
671
|
+
}
|
|
672
|
+
} catch {}
|
|
673
|
+
|
|
674
|
+
// Multi-model sophistication (do they switch models within sessions?)
|
|
675
|
+
try {
|
|
676
|
+
const daily = report.costAnalysis?.dailyCosts || [];
|
|
677
|
+
let multiModelDays = 0;
|
|
678
|
+
for (const d of daily) {
|
|
679
|
+
const models = (d.models || []).map(m => m.model);
|
|
680
|
+
const unique = new Set(models);
|
|
681
|
+
if (unique.size > 1) multiModelDays++;
|
|
682
|
+
}
|
|
683
|
+
data.multiModelDays = multiModelDays;
|
|
684
|
+
data.multiModelPct = daily.length > 0 ? Math.round(multiModelDays / daily.length * 100) : 0;
|
|
685
|
+
} catch {}
|
|
686
|
+
data.hasMakefile = existsSync(join(process.cwd(), 'Makefile'));
|
|
687
|
+
data.hasJustfile = existsSync(join(process.cwd(), 'justfile'));
|
|
688
|
+
|
|
689
|
+
// How many projects have CLAUDE.md
|
|
690
|
+
try {
|
|
691
|
+
const projDir = join(claudeDir, 'projects');
|
|
692
|
+
if (existsSync(projDir)) {
|
|
693
|
+
let claudeMdCount = 0;
|
|
694
|
+
for (const d of readdirSync(projDir).slice(0, 30)) {
|
|
695
|
+
// Check if a CLAUDE.md exists in the decoded project path
|
|
696
|
+
const decoded = d.replace(/^([A-Z])--/, '$1:/').replace(/-/g, '/');
|
|
697
|
+
if (existsSync(join(decoded, 'CLAUDE.md'))) claudeMdCount++;
|
|
698
|
+
}
|
|
699
|
+
data.projectsWithClaudeMd = claudeMdCount;
|
|
700
|
+
}
|
|
701
|
+
} catch {}
|
|
702
|
+
|
|
312
703
|
// First and last usage date (from JSONL file timestamps)
|
|
313
704
|
if (existsSync(projectsDir)) {
|
|
314
705
|
try {
|