clawculator 2.0.0 โ†’ 2.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,186 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ function severityColor(severity) {
8
+ return { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#38bdf8', info: '#22c55e' }[severity] || '#6b7280';
9
+ }
10
+
11
+ function severityBg(severity) {
12
+ return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', low: '#f0f9ff', info: '#f0fdf4' }[severity] || '#f9fafb';
13
+ }
14
+
15
+ function severityIcon(severity) {
16
+ return { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐Ÿ”ต', info: 'โœ…' }[severity] || 'โšช';
17
+ }
18
+
19
+ function relativeAge(ageMs) {
20
+ if (!ageMs) return 'unknown';
21
+ const s = Math.floor(ageMs / 1000);
22
+ if (s < 60) return `${s}s ago`;
23
+ const m = Math.floor(s / 60);
24
+ if (m < 60) return `${m}m ago`;
25
+ const h = Math.floor(m / 60);
26
+ if (h < 24) return `${h}h ago`;
27
+ const d = Math.floor(h / 24);
28
+ if (d < 30) return `${d}d ago`;
29
+ return `${Math.floor(d / 30)}mo ago`;
30
+ }
31
+
32
+ const SOURCE_LABELS = {
33
+ heartbeat: '๐Ÿ’“ Heartbeat', hooks: '๐Ÿช Hooks', whatsapp: '๐Ÿ“ฑ WhatsApp',
34
+ subagents: '๐Ÿค– Subagents', skills: '๐Ÿ”ง Skills', memory: '๐Ÿง  Memory',
35
+ primary_model: 'โš™๏ธ Primary Model', sessions: '๐Ÿ’ฌ Sessions',
36
+ workspace: '๐Ÿ“ Workspace', config: '๐Ÿ“„ Config',
37
+ };
38
+
39
+ async function generateHTMLReport(analysis, outPath) {
40
+ const { summary, findings, sessions } = analysis;
41
+ const bleed = summary.estimatedMonthlyBleed;
42
+
43
+ const findingCards = findings.map(f => `
44
+ <div class="finding" style="border-left: 4px solid ${severityColor(f.severity)}; background: ${severityBg(f.severity)}; padding: 16px; margin-bottom: 12px; border-radius: 0 8px 8px 0;">
45
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
46
+ <span style="font-size:18px">${severityIcon(f.severity)}</span>
47
+ <strong style="color:${severityColor(f.severity)}">${f.severity.toUpperCase()}</strong>
48
+ <span style="color:#6b7280; font-size:14px">${SOURCE_LABELS[f.source] || f.source}</span>
49
+ ${f.monthlyCost ? `<span style="margin-left:auto; color:${severityColor(f.severity)}; font-weight:bold">$${f.monthlyCost.toFixed(2)}/mo</span>` : ''}
50
+ </div>
51
+ <div style="font-weight:600; color:#111; margin-bottom:4px">${f.message}</div>
52
+ ${f.detail ? `<div style="color:#555; font-size:14px; margin-bottom:6px; white-space:pre-line">${f.detail}</div>` : ''}
53
+ ${f.fix ? `<div style="color:#16a34a; font-size:14px; margin-top:8px">โ†’ Fix: ${f.fix}</div>` : ''}
54
+ ${f.command ? `<div style="background:#0f172a; color:#7dd3fc; font-family:monospace; font-size:12px; padding:8px 12px; border-radius:6px; margin-top:8px">${f.command}</div>` : ''}
55
+ </div>
56
+ `).join('');
57
+
58
+ const sessionRows = (sessions || [])
59
+ .sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
60
+ .slice(0, 20)
61
+ .map(s => {
62
+ const keyDisplay = s.key.length > 18 ? s.key.slice(0, 16) + 'โ€ฆ' : s.key;
63
+ const flag = s.isOrphaned ? ' โš ๏ธ' : '';
64
+ const age = s.ageMs ? relativeAge(s.ageMs) : 'unknown';
65
+ const absDate = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '';
66
+ const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}/day` : 'โ€”';
67
+ return `
68
+ <tr style="${s.isOrphaned ? 'background:#fff7ed; color:#111;' : ''}">
69
+ <td style="padding:8px 12px; font-family:monospace; font-size:13px; ${s.isOrphaned ? 'color:#111;' : ''}">${keyDisplay}${flag}</td>
70
+ <td style="padding:8px 12px; ${s.isOrphaned ? 'color:#111;' : ''}">${s.modelLabel || s.model}</td>
71
+ <td style="padding:8px 12px; text-align:right; ${s.isOrphaned ? 'color:#111;' : ''}">${(s.inputTokens + s.outputTokens).toLocaleString()}</td>
72
+ <td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(6)}</td>
73
+ <td style="padding:8px 12px; text-align:right; color:#f59e0b">${daily}</td>
74
+ <td style="padding:8px 12px; color:#94a3b8; font-size:13px; ${s.isOrphaned ? 'color:#6b7280;' : ''}" title="${absDate}">${age}</td>
75
+ </tr>
76
+ `}).join('');
77
+
78
+ const html = `<!DOCTYPE html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="UTF-8">
82
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
+ <title>Clawculator Report โ€” ${new Date(analysis.scannedAt).toLocaleString()}</title>
84
+ <style>
85
+ * { box-sizing: border-box; margin: 0; padding: 0; }
86
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
87
+ .header { background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%); padding: 48px 32px; text-align: center; border-bottom: 1px solid #1e40af; }
88
+ .logo { font-size: 42px; font-weight: 900; letter-spacing: -2px; background: linear-gradient(90deg, #38bdf8, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
89
+ .tagline { color: #94a3b8; margin-top: 8px; font-size: 16px; }
90
+ .container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
91
+ .cards { display: table; width: 100%; border-spacing: 16px; border-collapse: separate; margin-bottom: 16px; }
92
+ .cards-row { display: table-row; }
93
+ .card { display: table-cell; background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; width: 16.6%; }
94
+ .card-value { font-size: 32px; font-weight: 800; }
95
+ .card-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
96
+ .section { background: #1e293b; border-radius: 12px; padding: 24px; margin-bottom: 24px; border: 1px solid #334155; }
97
+ .section-title { font-size: 18px; font-weight: 700; margin-bottom: 16px; color: #f1f5f9; }
98
+ table { width: 100%; border-collapse: collapse; }
99
+ th { background: #0f172a; padding: 10px 12px; text-align: left; font-size: 13px; color: #cbd5e1; font-weight: 600; }
100
+ td { color: #e2e8f0; }
101
+ tr:nth-child(even) { background: #0f172a33; }
102
+ .footer { text-align: center; color: #475569; font-size: 13px; padding: 32px; }
103
+ .bleed { background: linear-gradient(135deg, #7f1d1d, #991b1b); border-radius: 12px; padding: 20px 24px; margin-bottom: 24px; border: 1px solid #ef4444; }
104
+ .bleed-amount { font-size: 36px; font-weight: 900; color: #fca5a5; }
105
+ </style>
106
+ </head>
107
+ <body>
108
+ <div class="header">
109
+ <div class="logo">CLAWCULATOR</div>
110
+ <div class="tagline">Your friendly penny pincher. ยท 100% offline ยท Zero AI ยท Pure deterministic logic</div>
111
+ <div style="color:#64748b; font-size:13px; margin-top:12px">Report generated: ${new Date(analysis.scannedAt).toLocaleString()}</div>
112
+ </div>
113
+
114
+ <div class="container">
115
+ ${bleed > 0 ? `
116
+ <div class="bleed">
117
+ <div style="color:#fca5a5; font-size:14px; font-weight:600; margin-bottom:4px">โš ๏ธ ESTIMATED MONTHLY COST EXPOSURE</div>
118
+ <div class="bleed-amount">$${bleed.toFixed(2)}/month</div>
119
+ <div style="color:#fca5a5; font-size:14px; margin-top:4px">Based on current config โ€” fix the critical issues below to stop the bleed</div>
120
+ </div>` : `
121
+ <div style="background:#14532d; border-radius:12px; padding:20px 24px; margin-bottom:24px; border:1px solid #22c55e">
122
+ <div style="color:#86efac; font-size:18px; font-weight:700">โœ… No significant cost bleed detected</div>
123
+ </div>`}
124
+
125
+ <div class="cards"><div class="cards-row">
126
+ <div class="card">
127
+ <div class="card-value" style="color:#ef4444">${summary.critical}</div>
128
+ <div class="card-label">๐Ÿ”ด Critical</div>
129
+ </div>
130
+ <div class="card">
131
+ <div class="card-value" style="color:#f97316">${summary.high}</div>
132
+ <div class="card-label">๐ŸŸ  High</div>
133
+ </div>
134
+ <div class="card">
135
+ <div class="card-value" style="color:#eab308">${summary.medium}</div>
136
+ <div class="card-label">๐ŸŸก Medium</div>
137
+ </div>
138
+ <div class="card">
139
+ <div class="card-value" style="color:#38bdf8">${summary.low || 0}</div>
140
+ <div class="card-label">๐Ÿ”ต Low</div>
141
+ </div>
142
+ <div class="card">
143
+ <div class="card-value" style="color:#22c55e">${summary.info}</div>
144
+ <div class="card-label">โœ… OK</div>
145
+ </div>
146
+ <div class="card">
147
+ <div class="card-value" style="color:#38bdf8">${summary.sessionsAnalyzed}</div>
148
+ <div class="card-label">Sessions Analyzed</div>
149
+ </div>
150
+ <div class="card">
151
+ <div class="card-value" style="color:#818cf8">${(summary.totalTokensFound || 0).toLocaleString()}</div>
152
+ <div class="card-label">Total Tokens Found</div>
153
+ </div>
154
+ </div></div>
155
+
156
+ <div class="section">
157
+ <div class="section-title">Findings</div>
158
+ <div style="color:#0f172a">
159
+ ${findingCards}
160
+ </div>
161
+ </div>
162
+
163
+ ${sessionRows ? `
164
+ <div class="section">
165
+ <div class="section-title">Session Breakdown</div>
166
+ <div style="overflow-x:auto">
167
+ <table>
168
+ <thead><tr><th>Session</th><th>Model</th><th style="text-align:right">Tokens</th><th style="text-align:right">Total Cost</th><th style="text-align:right">$/day</th><th>Last Active</th></tr></thead>
169
+ <tbody>${sessionRows}</tbody>
170
+ </table>
171
+ </div>
172
+ </div>` : ''}
173
+
174
+ </div>
175
+ <div class="footer">
176
+ Clawculator ยท github.com/echoudhry/clawculator ยท Your friendly penny pincher.
177
+ </div>
178
+ </body>
179
+ </html>`;
180
+
181
+ const finalPath = outPath || path.join(os.tmpdir(), `clawculator-report-${Date.now()}.html`);
182
+ fs.writeFileSync(finalPath, html, 'utf8');
183
+ return finalPath;
184
+ }
185
+
186
+ module.exports = { generateHTMLReport };
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ const SEVERITY_ICON = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', info: 'โœ…' };
4
+ const SOURCE_LABELS = {
5
+ heartbeat: '๐Ÿ’“ Heartbeat', hooks: '๐Ÿช Hooks', whatsapp: '๐Ÿ“ฑ WhatsApp',
6
+ subagents: '๐Ÿค– Subagents', skills: '๐Ÿ”ง Skills', memory: '๐Ÿง  Memory',
7
+ primary_model: 'โš™๏ธ Primary Model', sessions: '๐Ÿ’ฌ Sessions',
8
+ workspace: '๐Ÿ“ Workspace', config: '๐Ÿ“„ Config',
9
+ };
10
+
11
+ function relativeAge(ageMs) {
12
+ if (!ageMs) return 'unknown';
13
+ const s = Math.floor(ageMs / 1000);
14
+ if (s < 60) return `${s}s ago`;
15
+ const m = Math.floor(s / 60);
16
+ if (m < 60) return `${m}m ago`;
17
+ const h = Math.floor(m / 60);
18
+ if (h < 24) return `${h}h ago`;
19
+ const d = Math.floor(h / 24);
20
+ if (d < 30) return `${d}d ago`;
21
+ const mo = Math.floor(d / 30);
22
+ return `${mo}mo ago`;
23
+ }
24
+
25
+ function absoluteDate(updatedAt) {
26
+ if (!updatedAt) return '';
27
+ return new Date(updatedAt).toLocaleString();
28
+ }
29
+
30
+ function generateMarkdownReport(analysis) {
31
+ const { summary, findings, sessions, primaryModel, scannedAt } = analysis;
32
+ const bleed = summary.estimatedMonthlyBleed;
33
+ const lines = [];
34
+
35
+ lines.push('# Clawculator Report');
36
+ lines.push('> Your friendly penny pincher. ยท 100% offline ยท Zero AI ยท Pure deterministic logic');
37
+ lines.push('');
38
+ lines.push(`**Scanned:** ${new Date(scannedAt).toLocaleString()}`);
39
+ if (primaryModel) lines.push(`**Primary model:** ${primaryModel}`);
40
+ lines.push('');
41
+
42
+ // Cost alert
43
+ if (bleed > 0) {
44
+ lines.push(`## โš ๏ธ Estimated Monthly Cost Exposure: $${bleed.toFixed(2)}/month`);
45
+ lines.push('');
46
+ } else {
47
+ lines.push('## โœ… No significant cost bleed detected');
48
+ lines.push('');
49
+ }
50
+
51
+ // Summary table
52
+ lines.push('## Summary');
53
+ lines.push('');
54
+ lines.push('| Metric | Value |');
55
+ lines.push('|--------|-------|');
56
+ lines.push(`| ๐Ÿ”ด Critical | ${summary.critical} |`);
57
+ lines.push(`| ๐ŸŸ  High | ${summary.high} |`);
58
+ lines.push(`| ๐ŸŸก Medium | ${summary.medium} |`);
59
+ lines.push(`| โœ… OK | ${summary.info} |`);
60
+ lines.push(`| Sessions analyzed | ${summary.sessionsAnalyzed} |`);
61
+ lines.push(`| Total tokens found | ${(summary.totalTokensFound || 0).toLocaleString()} |`);
62
+
63
+ // Session cost summary
64
+ if (sessions?.length > 0) {
65
+ const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
66
+ const dailyRates = sessions.filter(s => s.dailyCost).map(s => s.dailyCost);
67
+ const totalDailyRate = dailyRates.reduce((sum, r) => sum + r, 0);
68
+ if (totalCost > 0) lines.push(`| Total session cost (lifetime) | $${totalCost.toFixed(4)} |`);
69
+ if (totalDailyRate > 0) lines.push(`| Avg daily burn rate | $${totalDailyRate.toFixed(4)}/day (~$${(totalDailyRate * 30).toFixed(2)}/month) |`);
70
+ }
71
+ lines.push('');
72
+
73
+ // Findings by severity
74
+ lines.push('## Findings');
75
+ lines.push('');
76
+
77
+ for (const severity of ['critical', 'high', 'medium', 'info']) {
78
+ const group = findings.filter(f => f.severity === severity);
79
+ if (!group.length) continue;
80
+
81
+ lines.push(`### ${SEVERITY_ICON[severity]} ${severity.toUpperCase()} (${group.length})`);
82
+ lines.push('');
83
+
84
+ for (const f of group) {
85
+ lines.push(`#### ${SOURCE_LABELS[f.source] || f.source}`);
86
+ lines.push('');
87
+ lines.push(`**${f.message}**`);
88
+ lines.push('');
89
+ if (f.detail) lines.push(`${f.detail}`);
90
+ if (f.monthlyCost > 0) lines.push(`**Monthly cost:** $${f.monthlyCost.toFixed(2)}/month`);
91
+ if (f.fix) {
92
+ lines.push('');
93
+ lines.push(`**Fix:** ${f.fix}`);
94
+ }
95
+ if (f.command) {
96
+ lines.push('');
97
+ lines.push('```bash');
98
+ lines.push(f.command);
99
+ lines.push('```');
100
+ }
101
+ lines.push('');
102
+ }
103
+ }
104
+
105
+ // Session breakdown
106
+ if (sessions?.length > 0) {
107
+ lines.push('## Session Breakdown');
108
+ lines.push('');
109
+ lines.push('| Session | Model | Tokens | Total Cost | $/day | Last Active |');
110
+ lines.push('|---------|-------|--------|-----------|-------|-------------|');
111
+
112
+ [...sessions]
113
+ .sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
114
+ .slice(0, 20)
115
+ .forEach(s => {
116
+ const tok = (s.inputTokens + s.outputTokens).toLocaleString();
117
+ const flag = s.isOrphaned ? ' โš ๏ธ' : '';
118
+ const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โ€ฆ' : s.key;
119
+ const age = s.ageMs ? `${relativeAge(s.ageMs)} (${absoluteDate(s.updatedAt)})` : 'unknown';
120
+ const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ€”';
121
+ lines.push(`| \`${keyDisplay}${flag}\` | ${s.modelLabel || s.model} | ${tok} | $${s.cost.toFixed(6)} | ${daily} | ${age} |`);
122
+ });
123
+
124
+ lines.push('');
125
+ lines.push('> โš ๏ธ = orphaned session ยท $/day = total cost รท session age');
126
+ lines.push('');
127
+ }
128
+
129
+ // Quick wins
130
+ const wins = findings.filter(f => f.fix && f.severity !== 'info');
131
+ if (wins.length > 0) {
132
+ lines.push('## Quick Wins');
133
+ lines.push('');
134
+ wins.slice(0, 5).forEach((f, i) => {
135
+ lines.push(`${i + 1}. **${f.fix}**`);
136
+ if (f.command) lines.push(` \`${f.command}\``);
137
+ });
138
+ lines.push('');
139
+ }
140
+
141
+ lines.push('---');
142
+ lines.push('*Generated by [Clawculator](https://github.com/echoudhry/clawculator) ยท Your friendly penny pincher.*');
143
+
144
+ return lines.join('\n');
145
+ }
146
+
147
+ module.exports = { generateMarkdownReport };
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const SEVERITY_CONFIG = {
4
+ critical: { color: '\x1b[31m', icon: '๐Ÿ”ด', label: 'CRITICAL' },
5
+ high: { color: '\x1b[33m', icon: '๐ŸŸ ', label: 'HIGH' },
6
+ medium: { color: '\x1b[33m', icon: '๐ŸŸก', label: 'MEDIUM' },
7
+ low: { color: '\x1b[36m', icon: '๐Ÿ”ต', label: 'LOW' },
8
+ info: { color: '\x1b[32m', icon: 'โœ…', label: 'OK' },
9
+ };
10
+
11
+ const SOURCE_LABELS = {
12
+ heartbeat: '๐Ÿ’“ Heartbeat',
13
+ hooks: '๐Ÿช Hooks',
14
+ whatsapp: '๐Ÿ“ฑ WhatsApp',
15
+ subagents: '๐Ÿค– Subagents',
16
+ skills: '๐Ÿ”ง Skills',
17
+ memory: '๐Ÿง  Memory',
18
+ primary_model: 'โš™๏ธ Primary Model',
19
+ sessions: '๐Ÿ’ฌ Sessions',
20
+ workspace: '๐Ÿ“ Workspace',
21
+ context: '๐Ÿ“ Context Pruning',
22
+ vision: '๐Ÿ–ผ๏ธ Vision Tokens',
23
+ fallbacks: '๐Ÿ”€ Model Fallbacks',
24
+ multi_agent: '๐Ÿ‘ฅ Multi-Agent',
25
+ cron: 'โฐ Cron Jobs',
26
+ telegram: 'โœˆ๏ธ Telegram',
27
+ discord: '๐Ÿ’ฌ Discord',
28
+ signal: '๐Ÿ“ก Signal',
29
+ config: '๐Ÿ“„ Config',
30
+ };
31
+
32
+ function formatCost(cost) {
33
+ if (!cost) return '';
34
+ if (cost === 0) return '\x1b[32m$0.00/mo\x1b[0m';
35
+ if (cost < 1) return `\x1b[33m$${cost.toFixed(4)}/mo\x1b[0m`;
36
+ return `\x1b[31m$${cost.toFixed(2)}/mo\x1b[0m`;
37
+ }
38
+
39
+ function relativeAge(ageMs) {
40
+ if (!ageMs) return 'unknown';
41
+ const s = Math.floor(ageMs / 1000);
42
+ if (s < 60) return `${s}s ago`;
43
+ const m = Math.floor(s / 60);
44
+ if (m < 60) return `${m}m ago`;
45
+ const h = Math.floor(m / 60);
46
+ if (h < 24) return `${h}h ago`;
47
+ const d = Math.floor(h / 24);
48
+ if (d < 30) return `${d}d ago`;
49
+ return `${Math.floor(d / 30)}mo ago`;
50
+ }
51
+
52
+ function generateTerminalReport(analysis) {
53
+ const { summary, findings, sessions } = analysis;
54
+ const R = '\x1b[0m';
55
+ const B = '\x1b[1m';
56
+ const D = '\x1b[90m';
57
+ const C = '\x1b[36m';
58
+ const RED = '\x1b[31m';
59
+ const GRN = '\x1b[32m';
60
+
61
+ console.log(`${C}โ”โ”โ” Scan Complete โ”โ”โ”${R}`);
62
+ console.log(`${D}${new Date(analysis.scannedAt).toLocaleString()}${R}`);
63
+ if (analysis.primaryModel) console.log(`${D}Primary model: ${analysis.primaryModel}${R}`);
64
+ console.log();
65
+
66
+ const bleed = summary.estimatedMonthlyBleed;
67
+ if (bleed > 0) {
68
+ console.log(`${B}${RED}โš ๏ธ Estimated monthly cost exposure: $${bleed.toFixed(2)}/month${R}\n`);
69
+ }
70
+
71
+ for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
72
+ const group = findings.filter(f => f.severity === severity);
73
+ if (!group.length) continue;
74
+
75
+ const cfg = SEVERITY_CONFIG[severity];
76
+ console.log(`${cfg.color}${B}${cfg.icon} ${cfg.label} (${group.length})${R}`);
77
+ console.log(`${C}${'โ”€'.repeat(60)}${R}`);
78
+
79
+ for (const f of group) {
80
+ console.log(` ${B}${SOURCE_LABELS[f.source] || f.source}${R}`);
81
+ console.log(` ${f.message}`);
82
+ if (f.detail) console.log(` ${D}${f.detail}${R}`);
83
+ if (f.monthlyCost > 0) console.log(` ${D}Cost: ${R}${formatCost(f.monthlyCost)}`);
84
+ if (f.fix) console.log(` ${GRN}โ†’ ${f.fix}${R}`);
85
+ if (f.command) console.log(` ${D} ${f.command}${R}`);
86
+ console.log();
87
+ }
88
+ }
89
+
90
+ // Session breakdown
91
+ if (sessions?.length > 0) {
92
+ console.log(`${C}โ”โ”โ” Top Sessions by Token Usage โ”โ”โ”${R}\n`);
93
+ const sorted = [...sessions].sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens)).slice(0, 8);
94
+ console.log(` ${D}${'Session'.padEnd(16)} ${'Model'.padEnd(20)} ${'Tokens'.padEnd(10)} ${'Total Cost'.padEnd(12)} ${'$/day'.padEnd(10)} Last Active${R}`);
95
+ console.log(` ${D}${'โ”€'.repeat(95)}${R}`);
96
+ for (const s of sorted) {
97
+ const tok = (s.inputTokens + s.outputTokens).toLocaleString();
98
+ const flag = s.isOrphaned ? ' โš ๏ธ' : '';
99
+ const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โ€ฆ' : s.key;
100
+ const age = s.ageMs ? `${relativeAge(s.ageMs)} (${new Date(s.updatedAt).toLocaleDateString()})` : 'unknown';
101
+ const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ€”';
102
+ console.log(` ${(keyDisplay + flag).padEnd(16)} ${(s.modelLabel || s.model || 'unknown').slice(0, 20).padEnd(20)} ${tok.padEnd(10)} $${s.cost.toFixed(6).padEnd(11)} ${daily.padEnd(10)} ${D}${age}${R}`);
103
+ }
104
+
105
+ // Daily burn rate summary
106
+ const totalDailyRate = sessions.filter(s => s.dailyCost).reduce((sum, s) => sum + s.dailyCost, 0);
107
+ if (totalDailyRate > 0) {
108
+ console.log();
109
+ console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D} ยท ~$${(totalDailyRate * 30).toFixed(2)}/month${R}`);
110
+ }
111
+ console.log();
112
+ }
113
+
114
+ // Summary
115
+ console.log(`${C}โ”โ”โ” Summary โ”โ”โ”${R}`);
116
+ console.log(` ๐Ÿ”ด ${RED}${summary.critical}${R} critical ๐ŸŸ  ${summary.high} high ๐ŸŸก ${summary.medium} medium ๐Ÿ”ต ${summary.low||0} low โœ… ${summary.info} ok`);
117
+ console.log(` Sessions analyzed: ${summary.sessionsAnalyzed} ยท Tokens found: ${(summary.totalTokensFound||0).toLocaleString()}`);
118
+ if (bleed > 0) {
119
+ console.log(` ${RED}${B}Estimated monthly bleed: $${bleed.toFixed(2)}/month${R}`);
120
+ } else {
121
+ console.log(` ${GRN}No significant cost bleed detected โœ“${R}`);
122
+ }
123
+ console.log();
124
+
125
+ // Quick wins
126
+ const wins = findings.filter(f => f.fix && f.severity !== 'info');
127
+ if (wins.length > 0) {
128
+ console.log(`${C}โ”โ”โ” Quick Wins โ”โ”โ”${R}`);
129
+ wins.slice(0, 5).forEach((f, i) => {
130
+ console.log(` ${i + 1}. ${GRN}${f.fix}${R}`);
131
+ if (f.command) console.log(` ${D}${f.command}${R}`);
132
+ });
133
+ console.log();
134
+ }
135
+ }
136
+
137
+ module.exports = { generateTerminalReport };
138
+
139
+ // Source label additions for new sources
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { runAnalysis } = require('./analyzer');
5
+ const { generateTerminalReport } = require('./reporter');
6
+ const { generateMarkdownReport } = require('./mdReport');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const fs = require('fs');
10
+
11
+ const args = process.argv.slice(2);
12
+ const flags = {
13
+ json: args.includes('--json'),
14
+ md: args.includes('--md'),
15
+ help: args.includes('--help') || args.includes('-h'),
16
+ config: args.find(a => a.startsWith('--config='))?.split('=')[1],
17
+ out: args.find(a => a.startsWith('--out='))?.split('=')[1],
18
+ };
19
+
20
+ const BANNER = `
21
+ \x1b[36m
22
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
23
+ โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•
24
+ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
25
+ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
26
+ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ•”โ•โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
27
+ โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•šโ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•
28
+ \x1b[0m
29
+ \x1b[33mYour friendly penny pincher.\x1b[0m
30
+ \x1b[90m100% offline ยท Zero AI ยท Pure deterministic logic ยท Your data never leaves your machine\x1b[0m
31
+ `;
32
+
33
+ const HELP = `
34
+ Usage: node run.js [options]
35
+
36
+ Options:
37
+ (no flags) Full terminal analysis
38
+ --md Save markdown report to ./clawculator-report.md
39
+ --json Output raw JSON to stdout
40
+ --out=PATH Custom output path for --md
41
+ --config=PATH Path to openclaw.json (auto-detected by default)
42
+ --help, -h Show this help
43
+
44
+ Files read (read-only):
45
+ ~/.openclaw/openclaw.json
46
+ ~/.openclaw/agents/main/sessions/sessions.json
47
+ ~/clawd/ (file count only, no contents)
48
+ /tmp/openclaw (if present)
49
+
50
+ Files written (only when --md is used):
51
+ ./clawculator-report.md (or --out path)
52
+
53
+ No network requests are made. No data leaves your machine.
54
+ Session keys are truncated in all output to protect sensitive identifiers.
55
+ `;
56
+
57
+ async function main() {
58
+ if (flags.help) {
59
+ console.log(BANNER);
60
+ console.log(HELP);
61
+ process.exit(0);
62
+ }
63
+
64
+ console.log(BANNER);
65
+ console.log('\x1b[90mScanning your setup...\x1b[0m\n');
66
+
67
+ const configPath = flags.config || path.join(os.homedir(), '.openclaw', 'openclaw.json');
68
+ const sessionsPath = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions', 'sessions.json');
69
+ const logsDir = '/tmp/openclaw';
70
+
71
+ let analysis;
72
+ try {
73
+ analysis = await runAnalysis({ configPath, sessionsPath, logsDir });
74
+ } catch (err) {
75
+ console.error('\x1b[31mError:\x1b[0m', err.message);
76
+ process.exit(1);
77
+ }
78
+
79
+ if (flags.json) {
80
+ console.log(JSON.stringify(analysis, null, 2));
81
+ process.exit(0);
82
+ }
83
+
84
+ if (flags.md) {
85
+ const outPath = flags.out || path.join(process.cwd(), 'clawculator-report.md');
86
+ fs.writeFileSync(outPath, generateMarkdownReport(analysis), 'utf8');
87
+ console.log(`\x1b[32mโœ“ Markdown report saved:\x1b[0m ${outPath}`);
88
+ generateTerminalReport(analysis);
89
+ process.exit(0);
90
+ }
91
+
92
+ generateTerminalReport(analysis);
93
+ console.log('\x1b[90mโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\x1b[0m');
94
+ console.log('\x1b[36mClawculator\x1b[0m ยท github.com/echoudhry/clawculator ยท Your friendly penny pincher.');
95
+ console.log('\x1b[90mTip: --md saves a report your AI agent can read directly\x1b[0m');
96
+ console.log('\x1b[90mโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\x1b[0m\n');
97
+ }
98
+
99
+ main().catch(err => {
100
+ console.error('\x1b[31mFatal:\x1b[0m', err.message);
101
+ process.exit(1);
102
+ });
package/logo.png DELETED
Binary file