cchubber 0.1.0

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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Inflection Point Detection
3
+ * Finds the sharpest change in cache efficiency ratio over time.
4
+ * Outputs: "Your efficiency dropped 3.6x starting March 29. Before: 482:1. After: 1,726:1."
5
+ */
6
+ export function detectInflectionPoints(dailyFromJSONL) {
7
+ if (!dailyFromJSONL || dailyFromJSONL.length < 5) return null;
8
+
9
+ const sorted = [...dailyFromJSONL]
10
+ .filter(d => d.outputTokens > 0)
11
+ .sort((a, b) => a.date.localeCompare(b.date));
12
+
13
+ if (sorted.length < 5) return null;
14
+
15
+ // Sliding window: compare the average ratio of days before vs after each point
16
+ // Window size: at least 3 days on each side
17
+ const minWindow = 3;
18
+ let bestSplit = null;
19
+ let bestScore = 0;
20
+
21
+ for (let i = minWindow; i <= sorted.length - minWindow; i++) {
22
+ const before = sorted.slice(Math.max(0, i - 7), i);
23
+ const after = sorted.slice(i, Math.min(sorted.length, i + 7));
24
+
25
+ const beforeRatio = computeRatio(before);
26
+ const afterRatio = computeRatio(after);
27
+
28
+ if (beforeRatio === 0 || afterRatio === 0) continue;
29
+
30
+ // Score = magnitude of change (either direction)
31
+ const changeMultiplier = afterRatio > beforeRatio
32
+ ? afterRatio / beforeRatio
33
+ : beforeRatio / afterRatio;
34
+
35
+ if (changeMultiplier > bestScore && changeMultiplier >= 1.5) {
36
+ bestScore = changeMultiplier;
37
+ bestSplit = {
38
+ date: sorted[i].date,
39
+ beforeRatio,
40
+ afterRatio,
41
+ multiplier: Math.round(changeMultiplier * 10) / 10,
42
+ direction: afterRatio > beforeRatio ? 'worsened' : 'improved',
43
+ beforeDays: before.length,
44
+ afterDays: after.length,
45
+ };
46
+ }
47
+ }
48
+
49
+ if (!bestSplit) return null;
50
+
51
+ // Build human-readable summary
52
+ const dirLabel = bestSplit.direction === 'worsened' ? 'dropped' : 'improved';
53
+ bestSplit.summary = `Your cache efficiency ${dirLabel} ${bestSplit.multiplier}x starting ${formatDate(bestSplit.date)}. Before: ${bestSplit.beforeRatio.toLocaleString()}:1. After: ${bestSplit.afterRatio.toLocaleString()}:1.`;
54
+
55
+ return bestSplit;
56
+ }
57
+
58
+ function computeRatio(days) {
59
+ const totalOutput = days.reduce((s, d) => s + (d.outputTokens || 0), 0);
60
+ const totalCacheRead = days.reduce((s, d) => s + (d.cacheReadTokens || 0), 0);
61
+ return totalOutput > 0 ? Math.round(totalCacheRead / totalOutput) : 0;
62
+ }
63
+
64
+ function formatDate(dateStr) {
65
+ const d = new Date(dateStr + 'T00:00:00');
66
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
67
+ }
@@ -0,0 +1,128 @@
1
+ export function generateRecommendations(costAnalysis, cacheHealth, claudeMdStack, anomalies, inflection) {
2
+ const recs = [];
3
+
4
+ // 0. Inflection point — most important signal, goes first
5
+ if (inflection && inflection.direction === 'worsened' && inflection.multiplier >= 2) {
6
+ recs.push({
7
+ severity: 'critical',
8
+ title: `Efficiency dropped ${inflection.multiplier}x on ${inflection.date}`,
9
+ detail: inflection.summary,
10
+ action: 'This date likely correlates with a Claude Code update or cache regression. Check your CC version history. v2.1.89 had a known cache bug — v2.1.90 includes a fix.',
11
+ });
12
+ } else if (inflection && inflection.direction === 'improved' && inflection.multiplier >= 2) {
13
+ recs.push({
14
+ severity: 'positive',
15
+ title: `Efficiency improved ${inflection.multiplier}x on ${inflection.date}`,
16
+ detail: inflection.summary,
17
+ action: 'Something changed for the better on this date. Likely a version update or workflow change.',
18
+ });
19
+ }
20
+
21
+ // 1. CLAUDE.md size
22
+ if (claudeMdStack.totalTokensEstimate > 8000) {
23
+ recs.push({
24
+ severity: 'warning',
25
+ title: 'Large CLAUDE.md stack',
26
+ detail: `Your CLAUDE.md files total ~${claudeMdStack.totalTokensEstimate.toLocaleString()} tokens (${(claudeMdStack.totalBytes / 1024).toFixed(1)} KB). This is re-read on every message. At 200 messages/day, this costs ~$${claudeMdStack.costPerMessage.dailyCached200.toFixed(2)}/day cached, or $${claudeMdStack.costPerMessage.dailyUncached200.toFixed(2)}/day if cache breaks.`,
27
+ action: 'Review your CLAUDE.md for sections that could be moved to project-level files loaded on demand.',
28
+ });
29
+ }
30
+
31
+ // 2. Cache break frequency
32
+ if (cacheHealth.totalCacheBreaks > 10) {
33
+ const topReason = cacheHealth.reasonsRanked[0];
34
+ recs.push({
35
+ severity: cacheHealth.totalCacheBreaks > 50 ? 'critical' : 'warning',
36
+ title: `${cacheHealth.totalCacheBreaks} cache breaks detected`,
37
+ detail: `Each cache break forces a full context re-read at write prices (12.5x cache read cost). Top cause: "${topReason?.reason}" (${topReason?.count} times, ${topReason?.percentage}%).`,
38
+ action: topReason?.reason === 'Tool schemas changed'
39
+ ? 'Reduce MCP tool connections. Each tool add/remove invalidates the cache.'
40
+ : topReason?.reason === 'System prompt changed'
41
+ ? 'Avoid editing CLAUDE.md mid-session. Make changes between sessions.'
42
+ : topReason?.reason === 'TTL expiry'
43
+ ? 'Keep sessions active. Cache expires after 5 minutes of inactivity.'
44
+ : 'Review cache break logs in ~/.claude/tmp/cache-break-*.diff for details.',
45
+ });
46
+ }
47
+
48
+ // 3. High cache:output ratio
49
+ if (cacheHealth.efficiencyRatio > 2000) {
50
+ recs.push({
51
+ severity: 'critical',
52
+ title: `Cache efficiency ratio: ${cacheHealth.efficiencyRatio.toLocaleString()}:1`,
53
+ detail: `For every 1 token of output, ${cacheHealth.efficiencyRatio.toLocaleString()} tokens are read from cache. Healthy range is 300-800:1. This could indicate the known Claude Code cache bug (March 2026).`,
54
+ action: 'Check your Claude Code version. Versions around 2.1.85-2.1.90 have known cache regression bugs. Consider pinning to an earlier version.',
55
+ });
56
+ } else if (cacheHealth.efficiencyRatio > 1000) {
57
+ recs.push({
58
+ severity: 'warning',
59
+ title: `Elevated cache ratio: ${cacheHealth.efficiencyRatio.toLocaleString()}:1`,
60
+ detail: 'Above average but not critical. Could be large codebase exploration or heavy file reading.',
61
+ action: 'Use /compact more frequently in long sessions. Start fresh sessions for new tasks.',
62
+ });
63
+ }
64
+
65
+ // 4. Cost anomalies
66
+ if (anomalies.hasAnomalies) {
67
+ const spikes = anomalies.anomalies.filter(a => a.type === 'spike');
68
+ if (spikes.length > 0) {
69
+ const worst = spikes[0];
70
+ recs.push({
71
+ severity: worst.severity,
72
+ title: `${spikes.length} cost spike${spikes.length > 1 ? 's' : ''} detected`,
73
+ detail: `Worst: $${worst.cost.toFixed(2)} on ${worst.date} (${worst.zScore > 0 ? '+' : ''}${worst.deviation.toFixed(2)} from average of $${worst.avgCost.toFixed(2)}).${worst.cacheRatioAnomaly ? ' Cache ratio was also anomalous — likely cache bug impact.' : ''}`,
74
+ action: 'Compare session activity on spike days. Look for long sessions without /compact, or sessions where many MCP tools were connected.',
75
+ });
76
+ }
77
+ }
78
+
79
+ // 5. Cost trend
80
+ if (anomalies.trend === 'rising_fast') {
81
+ recs.push({
82
+ severity: 'critical',
83
+ title: 'Costs rising rapidly',
84
+ detail: 'Your recent 7-day average is significantly higher than your historical average.',
85
+ action: 'This may be related to the March 2026 Claude Code cache bug. Check Anthropic status for updates.',
86
+ });
87
+ }
88
+
89
+ // 6. Opus dominance
90
+ const modelCosts = costAnalysis.modelCosts || {};
91
+ const totalModelCost = Object.values(modelCosts).reduce((s, c) => s + c, 0);
92
+ const opusCost = Object.entries(modelCosts)
93
+ .filter(([name]) => name.includes('opus'))
94
+ .reduce((s, [, c]) => s + c, 0);
95
+ const opusPercentage = totalModelCost > 0 ? (opusCost / totalModelCost) * 100 : 0;
96
+
97
+ if (opusPercentage > 90) {
98
+ recs.push({
99
+ severity: 'info',
100
+ title: `${Math.round(opusPercentage)}% of costs from Opus`,
101
+ detail: 'Opus is the most expensive model. Subagents and simple tasks could use Sonnet or Haiku.',
102
+ action: 'Set model: "sonnet" or "haiku" on Task tool calls for search, documentation lookup, and log analysis.',
103
+ });
104
+ }
105
+
106
+ // 7. Session depth — long sessions without compact
107
+ const sessions = costAnalysis.sessions || {};
108
+ if (sessions.avgDurationMinutes > 60) {
109
+ recs.push({
110
+ severity: 'warning',
111
+ title: `Average session: ${Math.round(sessions.avgDurationMinutes)} minutes`,
112
+ detail: `Long sessions accumulate context that degrades both performance and cache efficiency. Sessions over 60 minutes often benefit from /compact.`,
113
+ action: 'Use /compact every 30-40 tool calls or when switching tasks. Start fresh sessions for new work.',
114
+ });
115
+ }
116
+
117
+ // 8. Caching savings acknowledgment
118
+ if (cacheHealth.savings.fromCaching > 100) {
119
+ recs.push({
120
+ severity: 'positive',
121
+ title: `Caching saved you ~$${cacheHealth.savings.fromCaching.toLocaleString()}`,
122
+ detail: 'Without prompt caching, your bill would be significantly higher. The cache system is working — the question is whether it breaks too often.',
123
+ action: 'No action needed. Keep sessions alive to maximize cache hits.',
124
+ });
125
+ }
126
+
127
+ return recs;
128
+ }
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve, join } from 'path';
4
+ import { existsSync, writeFileSync } from 'fs';
5
+ import { homedir, platform } from 'os';
6
+ import { exec } from 'child_process';
7
+
8
+ import { readAllJSONL, aggregateDaily, aggregateByModel, aggregateByProject } from '../readers/jsonl-reader.js';
9
+ import { readStatsCache } from '../readers/stats-cache.js';
10
+ import { readSessionMeta } from '../readers/session-meta.js';
11
+ import { readCacheBreaks } from '../readers/cache-breaks.js';
12
+ import { readClaudeMdStack } from '../readers/claude-md.js';
13
+ import { readOAuthUsage } from '../readers/oauth-usage.js';
14
+ import { analyzeUsage, fetchPricing } from '../analyzers/cost-calculator.js';
15
+ import { analyzeCacheHealth } from '../analyzers/cache-health.js';
16
+ import { detectAnomalies } from '../analyzers/anomaly-detector.js';
17
+ import { generateRecommendations } from '../analyzers/recommendations.js';
18
+ import { detectInflectionPoints } from '../analyzers/inflection-detector.js';
19
+ import { renderHTML } from '../renderers/html-report.js';
20
+ import { renderTerminal } from '../renderers/terminal-summary.js';
21
+
22
+ const args = process.argv.slice(2);
23
+ const flags = {
24
+ help: args.includes('--help') || args.includes('-h'),
25
+ json: args.includes('--json'),
26
+ noOpen: args.includes('--no-open'),
27
+ output: (() => {
28
+ const idx = args.indexOf('--output') !== -1 ? args.indexOf('--output') : args.indexOf('-o');
29
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
30
+ })(),
31
+ days: (() => {
32
+ const idx = args.indexOf('--days') !== -1 ? args.indexOf('--days') : args.indexOf('-d');
33
+ return idx !== -1 && args[idx + 1] ? parseInt(args[idx + 1], 10) : 30;
34
+ })(),
35
+ };
36
+
37
+ if (flags.help) {
38
+ console.log(`
39
+ ╔═══════════════════════════════════════════════╗
40
+ ║ CC Hubber v0.1.0 ║
41
+ ║ What you spent. Why you spent it. Is that ║
42
+ ║ normal. ║
43
+ ╚═══════════════════════════════════════════════╝
44
+
45
+ Usage: cchubber [options]
46
+
47
+ Options:
48
+ --days, -d <n> Analyze last N days (default: 30)
49
+ --output, -o <path> Output HTML report to custom path
50
+ --no-open Don't auto-open the report in browser
51
+ --json Output raw analysis as JSON
52
+ -h, --help Show this help
53
+
54
+ Examples:
55
+ cchubber Scan & open HTML report
56
+ cchubber --days 7 Last 7 days only
57
+ cchubber -o report.html Custom output path
58
+ cchubber --json Machine-readable output
59
+
60
+ Shipped with Mover OS at speed.
61
+ https://moveros.dev
62
+ `);
63
+ process.exit(0);
64
+ }
65
+
66
+ async function main() {
67
+ const claudeDir = getClaudeDir();
68
+
69
+ if (!existsSync(claudeDir)) {
70
+ console.error('\n ✗ Claude Code data directory not found at: ' + claudeDir);
71
+ console.error(' Make sure Claude Code is installed and has been used at least once.\n');
72
+ process.exit(1);
73
+ }
74
+
75
+ console.log('\n CC Hubber v0.1.0');
76
+ console.log(' ─────────────────────────────');
77
+ console.log(' Reading local Claude Code data...\n');
78
+
79
+ // Read all data sources
80
+ const jsonlEntries = readAllJSONL(claudeDir);
81
+ const statsCache = readStatsCache(claudeDir);
82
+ const sessionMeta = readSessionMeta(claudeDir);
83
+ const cacheBreaks = readCacheBreaks(claudeDir);
84
+ const claudeMdStack = readClaudeMdStack(claudeDir);
85
+ const oauthUsage = await readOAuthUsage(claudeDir);
86
+
87
+ if (jsonlEntries.length === 0 && !statsCache) {
88
+ console.error(' ✗ No usage data found. Use Claude Code first, then run CC Hubber.\n');
89
+ process.exit(1);
90
+ }
91
+
92
+ // Aggregate JSONL into daily + model + project views (primary data source)
93
+ const dailyFromJSONL = aggregateDaily(jsonlEntries);
94
+ const modelFromJSONL = aggregateByModel(jsonlEntries);
95
+ const projectBreakdown = aggregateByProject(jsonlEntries, claudeDir);
96
+
97
+ // Fetch dynamic pricing (LiteLLM) with hardcoded fallback
98
+ const pricing = await fetchPricing();
99
+ const pricingSource = pricing === null ? 'hardcoded' : 'LiteLLM';
100
+
101
+ console.log(` ✓ ${jsonlEntries.length.toLocaleString()} conversation entries parsed`);
102
+ console.log(` ✓ ${dailyFromJSONL.length} days of data found`);
103
+ console.log(` ✓ Pricing: ${pricingSource}`);
104
+ console.log(` ✓ ${sessionMeta.length} sessions found`);
105
+ console.log(` ✓ ${cacheBreaks.length} cache break events found`);
106
+ console.log(` ✓ CLAUDE.md stack: ${claudeMdStack.totalTokensEstimate.toLocaleString()} tokens (~${(claudeMdStack.totalBytes / 1024).toFixed(1)} KB)`);
107
+ if (oauthUsage) console.log(' ✓ Live rate limits loaded');
108
+ else console.log(' ○ Live rate limits skipped (no OAuth token)');
109
+
110
+ // Analyze — use ALL data for the HTML (client-side JS handles filtering)
111
+ // The --days flag sets the default view, but all data is embedded
112
+ console.log('\n Analyzing...\n');
113
+ const allTimeDays = 99999; // Pass everything to the report
114
+ const costAnalysis = analyzeUsage(statsCache, sessionMeta, allTimeDays, dailyFromJSONL, modelFromJSONL);
115
+ const cacheHealth = analyzeCacheHealth(statsCache, cacheBreaks, allTimeDays, dailyFromJSONL);
116
+ const anomalies = detectAnomalies(costAnalysis);
117
+ const inflection = detectInflectionPoints(dailyFromJSONL);
118
+ const recommendations = generateRecommendations(costAnalysis, cacheHealth, claudeMdStack, anomalies, inflection);
119
+
120
+ if (inflection) {
121
+ console.log(` ✓ Inflection point: ${inflection.summary}`);
122
+ }
123
+
124
+ console.log(` ✓ ${projectBreakdown.length} projects detected`);
125
+
126
+ const report = {
127
+ generatedAt: new Date().toISOString(),
128
+ periodDays: flags.days, // Default view in HTML
129
+ costAnalysis,
130
+ cacheHealth,
131
+ anomalies,
132
+ inflection,
133
+ projectBreakdown,
134
+ claudeMdStack,
135
+ oauthUsage,
136
+ recommendations,
137
+ };
138
+
139
+ // Output
140
+ if (flags.json) {
141
+ console.log(JSON.stringify(report, null, 2));
142
+ return;
143
+ }
144
+
145
+ renderTerminal(report);
146
+
147
+ const outputPath = flags.output || join(process.cwd(), 'cchubber-report.html');
148
+ const html = renderHTML(report);
149
+ writeFileSync(outputPath, html, 'utf-8');
150
+ console.log(`\n ✓ Report saved to: ${outputPath}`);
151
+
152
+ if (!flags.noOpen) {
153
+ openInBrowser(outputPath);
154
+ console.log(' ✓ Opened in browser\n');
155
+ }
156
+ }
157
+
158
+ function getClaudeDir() {
159
+ const home = homedir();
160
+ return join(home, '.claude');
161
+ }
162
+
163
+ function openInBrowser(filePath) {
164
+ const p = platform();
165
+ const cmd = p === 'win32' ? `start "" "${filePath}"`
166
+ : p === 'darwin' ? `open "${filePath}"`
167
+ : `xdg-open "${filePath}"`;
168
+ exec(cmd, (err) => { if (err) console.log(' ○ Could not auto-open browser. Open the file manually.'); });
169
+ }
170
+
171
+ main().catch((err) => {
172
+ console.error('\n ✗ Error:', err.message);
173
+ process.exit(1);
174
+ });
@@ -0,0 +1,81 @@
1
+ import { readdirSync, readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function readCacheBreaks(claudeDir) {
5
+ const tmpDir = join(claudeDir, 'tmp');
6
+ if (!existsSync(tmpDir)) return [];
7
+
8
+ const breaks = [];
9
+
10
+ try {
11
+ const files = readdirSync(tmpDir).filter(f => f.startsWith('cache-break-') && f.endsWith('.diff'));
12
+
13
+ for (const file of files) {
14
+ try {
15
+ const raw = readFileSync(join(tmpDir, file), 'utf-8');
16
+ const parsed = parseCacheBreakDiff(raw, file);
17
+ if (parsed) breaks.push(parsed);
18
+ } catch {
19
+ // Skip unreadable files
20
+ }
21
+ }
22
+ } catch {
23
+ return [];
24
+ }
25
+
26
+ return breaks.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
27
+ }
28
+
29
+ function parseCacheBreakDiff(content, filename) {
30
+ // Extract timestamp from filename: cache-break-<timestamp>.diff
31
+ const tsMatch = filename.match(/cache-break-(\d+)/);
32
+ const timestamp = tsMatch ? new Date(parseInt(tsMatch[1])).toISOString() : null;
33
+
34
+ // Parse the diff content for reasons
35
+ const reasons = [];
36
+ const lines = content.split('\n');
37
+
38
+ // Known cache break reason patterns from source code
39
+ const reasonPatterns = [
40
+ { pattern: /system.?prompt.?changed/i, reason: 'System prompt changed' },
41
+ { pattern: /tool.?schema.?changed/i, reason: 'Tool schemas changed' },
42
+ { pattern: /model.?changed/i, reason: 'Model changed' },
43
+ { pattern: /fast.?mode/i, reason: 'Fast mode toggled' },
44
+ { pattern: /cache.?strategy.?changed/i, reason: 'Cache strategy changed' },
45
+ { pattern: /cache.?control.?changed/i, reason: 'Cache control changed' },
46
+ { pattern: /betas?.?changed/i, reason: 'Betas header changed' },
47
+ { pattern: /auto.?mode/i, reason: 'Auto mode toggled' },
48
+ { pattern: /overage/i, reason: 'Overage state changed' },
49
+ { pattern: /microcompact/i, reason: 'Cached microcompact toggled' },
50
+ { pattern: /effort/i, reason: 'Effort value changed' },
51
+ { pattern: /extra.?body/i, reason: 'Extra body params changed' },
52
+ { pattern: /ttl/i, reason: 'TTL expiry' },
53
+ { pattern: /server.?side|evict/i, reason: 'Server-side eviction' },
54
+ ];
55
+
56
+ for (const line of lines) {
57
+ for (const { pattern, reason } of reasonPatterns) {
58
+ if (pattern.test(line) && !reasons.includes(reason)) {
59
+ reasons.push(reason);
60
+ }
61
+ }
62
+ }
63
+
64
+ // If no specific reason detected, mark as unknown
65
+ if (reasons.length === 0) {
66
+ reasons.push('Unknown / Server-side');
67
+ }
68
+
69
+ // Try to extract token counts from the diff
70
+ const prevCacheMatch = content.match(/prev(?:ious)?.*?cache.*?(\d[\d,]*)/i);
71
+ const newCacheMatch = content.match(/new.*?cache.*?(\d[\d,]*)/i);
72
+
73
+ return {
74
+ timestamp,
75
+ filename,
76
+ reasons,
77
+ rawContent: content.slice(0, 500), // Keep first 500 chars for debugging
78
+ prevCacheTokens: prevCacheMatch ? parseInt(prevCacheMatch[1].replace(/,/g, '')) : null,
79
+ newCacheTokens: newCacheMatch ? parseInt(newCacheMatch[1].replace(/,/g, '')) : null,
80
+ };
81
+ }
@@ -0,0 +1,67 @@
1
+ import { readFileSync, existsSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ export function readClaudeMdStack(claudeDir) {
6
+ const home = homedir();
7
+ const stack = [];
8
+
9
+ // Global CLAUDE.md
10
+ const globalPath = join(home, '.claude', 'CLAUDE.md');
11
+ if (existsSync(globalPath)) {
12
+ const stat = statSync(globalPath);
13
+ stack.push({
14
+ level: 'global',
15
+ path: globalPath,
16
+ bytes: stat.size,
17
+ tokensEstimate: Math.round(stat.size / 4),
18
+ });
19
+ }
20
+
21
+ // Try to find project-level CLAUDE.md by walking up from cwd
22
+ let dir = process.cwd();
23
+ const checked = new Set();
24
+ while (dir && !checked.has(dir)) {
25
+ checked.add(dir);
26
+ const projectMd = join(dir, 'CLAUDE.md');
27
+ if (existsSync(projectMd) && projectMd !== globalPath) {
28
+ const stat = statSync(projectMd);
29
+ stack.push({
30
+ level: 'project',
31
+ path: projectMd,
32
+ bytes: stat.size,
33
+ tokensEstimate: Math.round(stat.size / 4),
34
+ });
35
+ }
36
+ const parent = join(dir, '..');
37
+ if (parent === dir) break;
38
+ dir = parent;
39
+ }
40
+
41
+ // Check for .claude/settings.json to understand hook/skill overhead
42
+ const settingsPath = join(home, '.claude', 'settings.json');
43
+ let settingsSize = 0;
44
+ if (existsSync(settingsPath)) {
45
+ settingsSize = statSync(settingsPath).size;
46
+ }
47
+
48
+ const totalBytes = stack.reduce((sum, f) => sum + f.bytes, 0);
49
+ const totalTokensEstimate = stack.reduce((sum, f) => sum + f.tokensEstimate, 0);
50
+
51
+ // Estimate per-message cost at different cache rates (Opus 4.6)
52
+ const cachedCostPerMsg = totalTokensEstimate * 0.0000005; // $0.50/M cache read
53
+ const uncachedCostPerMsg = totalTokensEstimate * 0.000005; // $5.00/M standard input
54
+
55
+ return {
56
+ files: stack,
57
+ totalBytes,
58
+ totalTokensEstimate,
59
+ settingsBytes: settingsSize,
60
+ costPerMessage: {
61
+ cached: cachedCostPerMsg,
62
+ uncached: uncachedCostPerMsg,
63
+ dailyCached200: cachedCostPerMsg * 200,
64
+ dailyUncached200: uncachedCostPerMsg * 200,
65
+ },
66
+ };
67
+ }