clawculator 2.1.2 โ 2.1.4
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/clawculator-sync.patch +452 -0
- package/package.json +1 -1
- package/skills/clawculator/htmlReport.js +30 -6
- package/skills/clawculator/mdReport.js +3 -2
- package/src/analyzer.js +32 -5
- package/src/htmlReport.js +55 -10
- package/src/mdReport.js +43 -9
- package/src/reporter.js +32 -8
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
diff --git a/skills/clawculator/htmlReport.js b/skills/clawculator/htmlReport.js
|
|
2
|
+
index 6b944d1..05f9616 100644
|
|
3
|
+
--- a/skills/clawculator/htmlReport.js
|
|
4
|
+
+++ b/skills/clawculator/htmlReport.js
|
|
5
|
+
@@ -5,15 +5,15 @@ const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
function severityColor(severity) {
|
|
9
|
+
- return { critical: '#ef4444', high: '#f97316', medium: '#eab308', info: '#22c55e' }[severity] || '#6b7280';
|
|
10
|
+
+ return { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#06b6d4', info: '#22c55e' }[severity] || '#6b7280';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function severityBg(severity) {
|
|
14
|
+
- return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
15
|
+
+ return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', low: '#ecfeff', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function severityIcon(severity) {
|
|
19
|
+
- return { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' }[severity] || 'โช';
|
|
20
|
+
+ return { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' }[severity] || 'โช';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function relativeAge(ageMs) {
|
|
24
|
+
@@ -40,6 +40,25 @@ async function generateHTMLReport(analysis, outPath) {
|
|
25
|
+
const { summary, findings, sessions } = analysis;
|
|
26
|
+
const bleed = summary.estimatedMonthlyBleed;
|
|
27
|
+
|
|
28
|
+
+ // Burn summary calculation
|
|
29
|
+
+ const activeSessions = (sessions || []).filter(s => s.dailyCost);
|
|
30
|
+
+ const totalDaily = activeSessions.reduce((sum, s) => sum + (s.dailyCost || 0), 0);
|
|
31
|
+
+ const totalMonthly = totalDaily * 30;
|
|
32
|
+
+ const burnSummary = totalDaily > 0 ? `
|
|
33
|
+
+ <div class="section">
|
|
34
|
+
+ <div class="section-title">Session Burn Rate</div>
|
|
35
|
+
+ <div style="display:flex; gap:32px; flex-wrap:wrap;">
|
|
36
|
+
+ <div>
|
|
37
|
+
+ <div style="font-size:24px; font-weight:800; color:#f59e0b;">$${totalDaily.toFixed(4)}/day</div>
|
|
38
|
+
+ <div style="font-size:13px; color:#94a3b8; margin-top:4px;">Combined daily burn rate across ${activeSessions.length} active session${activeSessions.length !== 1 ? 's' : ''}</div>
|
|
39
|
+
+ </div>
|
|
40
|
+
+ <div>
|
|
41
|
+
+ <div style="font-size:24px; font-weight:800; color:#f97316;">$${totalMonthly.toFixed(2)}/month</div>
|
|
42
|
+
+ <div style="font-size:13px; color:#94a3b8; margin-top:4px;">Projected monthly from active sessions</div>
|
|
43
|
+
+ </div>
|
|
44
|
+
+ </div>
|
|
45
|
+
+ </div>` : '';
|
|
46
|
+
+
|
|
47
|
+
const findingCards = findings.map(f => `
|
|
48
|
+
<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;">
|
|
49
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
|
|
50
|
+
@@ -87,7 +106,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
51
|
+
.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; }
|
|
52
|
+
.tagline { color: #94a3b8; margin-top: 8px; font-size: 16px; }
|
|
53
|
+
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
54
|
+
- .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
55
|
+
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
56
|
+
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
57
|
+
.card-value { font-size: 32px; font-weight: 800; }
|
|
58
|
+
.card-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
|
59
|
+
@@ -132,6 +151,10 @@ async function generateHTMLReport(analysis, outPath) {
|
|
60
|
+
<div class="card-value" style="color:#eab308">${summary.medium}</div>
|
|
61
|
+
<div class="card-label">๐ก Medium</div>
|
|
62
|
+
</div>
|
|
63
|
+
+ <div class="card">
|
|
64
|
+
+ <div class="card-value" style="color:#06b6d4">${summary.low || 0}</div>
|
|
65
|
+
+ <div class="card-label">๐ต Low</div>
|
|
66
|
+
+ </div>
|
|
67
|
+
<div class="card">
|
|
68
|
+
<div class="card-value" style="color:#22c55e">${summary.info}</div>
|
|
69
|
+
<div class="card-label">โ
OK</div>
|
|
70
|
+
@@ -162,7 +185,8 @@ async function generateHTMLReport(analysis, outPath) {
|
|
71
|
+
<tbody>${sessionRows}</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
</div>
|
|
74
|
+
- </div>` : ''}
|
|
75
|
+
+ </div>
|
|
76
|
+
+ ${burnSummary}` : ''}
|
|
77
|
+
|
|
78
|
+
</div>
|
|
79
|
+
<div class="footer">
|
|
80
|
+
@@ -172,7 +196,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
81
|
+
</html>`;
|
|
82
|
+
|
|
83
|
+
if (!outPath) {
|
|
84
|
+
- outPath = path.join(os.tmpdir(), `clawculator-report-${Date.now()}.html`);
|
|
85
|
+
+ outPath = path.join(process.cwd(), `clawculator-report-${Date.now()}.html`);
|
|
86
|
+
}
|
|
87
|
+
fs.writeFileSync(outPath, html, 'utf8');
|
|
88
|
+
return outPath;
|
|
89
|
+
diff --git a/skills/clawculator/mdReport.js b/skills/clawculator/mdReport.js
|
|
90
|
+
index 0f8971c..7a0a839 100644
|
|
91
|
+
--- a/skills/clawculator/mdReport.js
|
|
92
|
+
+++ b/skills/clawculator/mdReport.js
|
|
93
|
+
@@ -1,6 +1,6 @@
|
|
94
|
+
'use strict';
|
|
95
|
+
|
|
96
|
+
-const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' };
|
|
97
|
+
+const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' };
|
|
98
|
+
const SOURCE_LABELS = {
|
|
99
|
+
heartbeat: '๐ Heartbeat', hooks: '๐ช Hooks', whatsapp: '๐ฑ WhatsApp',
|
|
100
|
+
subagents: '๐ค Subagents', skills: '๐ง Skills', memory: '๐ง Memory',
|
|
101
|
+
@@ -56,6 +56,7 @@ function generateMarkdownReport(analysis) {
|
|
102
|
+
lines.push(`| ๐ด Critical | ${summary.critical} |`);
|
|
103
|
+
lines.push(`| ๐ High | ${summary.high} |`);
|
|
104
|
+
lines.push(`| ๐ก Medium | ${summary.medium} |`);
|
|
105
|
+
+ lines.push(`| ๐ต Low | ${summary.low || 0} |`);
|
|
106
|
+
lines.push(`| โ
OK | ${summary.info} |`);
|
|
107
|
+
lines.push(`| Sessions analyzed | ${summary.sessionsAnalyzed} |`);
|
|
108
|
+
lines.push(`| Total tokens found | ${(summary.totalTokensFound || 0).toLocaleString()} |`);
|
|
109
|
+
@@ -74,7 +75,7 @@ function generateMarkdownReport(analysis) {
|
|
110
|
+
lines.push('## Findings');
|
|
111
|
+
lines.push('');
|
|
112
|
+
|
|
113
|
+
- for (const severity of ['critical', 'high', 'medium', 'info']) {
|
|
114
|
+
+ for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
115
|
+
const group = findings.filter(f => f.severity === severity);
|
|
116
|
+
if (!group.length) continue;
|
|
117
|
+
|
|
118
|
+
diff --git a/src/analyzer.js b/src/analyzer.js
|
|
119
|
+
index 9c5b3b9..4659b5c 100644
|
|
120
|
+
--- a/src/analyzer.js
|
|
121
|
+
+++ b/src/analyzer.js
|
|
122
|
+
@@ -143,7 +143,12 @@ function isLocalModel(modelStr) {
|
|
123
|
+
|
|
124
|
+
function isHaikuTier(modelStr) {
|
|
125
|
+
if (!modelStr) return false;
|
|
126
|
+
- return modelStr.toLowerCase().includes('haiku');
|
|
127
|
+
+ const lower = modelStr.toLowerCase();
|
|
128
|
+
+ return lower.includes('haiku');
|
|
129
|
+
+}
|
|
130
|
+
+
|
|
131
|
+
+function isAcceptableForHooks(modelStr) {
|
|
132
|
+
+ return isLocalModel(modelStr) || isHaikuTier(modelStr);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isOpenRouter(modelStr) {
|
|
136
|
+
@@ -523,7 +528,11 @@ function analyzeSessions(sessionsPath) {
|
|
137
|
+
if (isOrphaned) orphaned.push({ key, model, tokens: inTok + outTok, cost });
|
|
138
|
+
if (inTok + outTok > 50000) large.push({ key, model, tokens: inTok + outTok });
|
|
139
|
+
|
|
140
|
+
- breakdown.push({ key, model, modelLabel: modelKey ? (MODEL_PRICING[modelKey]?.label || modelKey) : 'unknown', inputTokens: inTok, outputTokens: outTok, cost, updatedAt, isOrphaned });
|
|
141
|
+
+ const ageMs = updatedAt ? Date.now() - new Date(updatedAt).getTime() : null;
|
|
142
|
+
+ const ageDays = ageMs ? ageMs / (1000 * 3600 * 24) : null;
|
|
143
|
+
+ const dailyCost = (ageDays && ageDays > 0.01 && cost > 0) ? cost / ageDays : null;
|
|
144
|
+
+
|
|
145
|
+
+ breakdown.push({ key, model, modelLabel: modelKey ? (MODEL_PRICING[modelKey]?.label || modelKey) : 'unknown', inputTokens: inTok, outputTokens: outTok, cost, updatedAt, ageMs, dailyCost, isOrphaned });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (orphaned.length > 0) findings.push({ severity: 'high', source: 'sessions', message: `${orphaned.length} orphaned session(s) โ still holding tokens on paid models`, detail: orphaned.map(s => `${s.key}: ${s.tokens.toLocaleString()} tokens ($${s.cost.toFixed(4)})`).join('\n '), ...FIXES.ORPHANED_SESSIONS });
|
|
149
|
+
@@ -575,6 +584,7 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
150
|
+
critical: allFindings.filter(f => f.severity === 'critical').length,
|
|
151
|
+
high: allFindings.filter(f => f.severity === 'high').length,
|
|
152
|
+
medium: allFindings.filter(f => f.severity === 'medium').length,
|
|
153
|
+
+ low: allFindings.filter(f => f.severity === 'low').length,
|
|
154
|
+
info: allFindings.filter(f => f.severity === 'info').length,
|
|
155
|
+
estimatedMonthlyBleed,
|
|
156
|
+
sessionsAnalyzed: sessionResult.sessionCount,
|
|
157
|
+
diff --git a/src/htmlReport.js b/src/htmlReport.js
|
|
158
|
+
index 30cae47..05f9616 100644
|
|
159
|
+
--- a/src/htmlReport.js
|
|
160
|
+
+++ b/src/htmlReport.js
|
|
161
|
+
@@ -5,15 +5,28 @@ const path = require('path');
|
|
162
|
+
const os = require('os');
|
|
163
|
+
|
|
164
|
+
function severityColor(severity) {
|
|
165
|
+
- return { critical: '#ef4444', high: '#f97316', medium: '#eab308', info: '#22c55e' }[severity] || '#6b7280';
|
|
166
|
+
+ return { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#06b6d4', info: '#22c55e' }[severity] || '#6b7280';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function severityBg(severity) {
|
|
170
|
+
- return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
171
|
+
+ return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', low: '#ecfeff', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function severityIcon(severity) {
|
|
175
|
+
- return { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' }[severity] || 'โช';
|
|
176
|
+
+ return { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' }[severity] || 'โช';
|
|
177
|
+
+}
|
|
178
|
+
+
|
|
179
|
+
+function relativeAge(ageMs) {
|
|
180
|
+
+ if (!ageMs) return 'unknown';
|
|
181
|
+
+ const s = Math.floor(ageMs / 1000);
|
|
182
|
+
+ if (s < 60) return `${s}s ago`;
|
|
183
|
+
+ const m = Math.floor(s / 60);
|
|
184
|
+
+ if (m < 60) return `${m}m ago`;
|
|
185
|
+
+ const h = Math.floor(m / 60);
|
|
186
|
+
+ if (h < 24) return `${h}h ago`;
|
|
187
|
+
+ const d = Math.floor(h / 24);
|
|
188
|
+
+ if (d < 30) return `${d}d ago`;
|
|
189
|
+
+ return `${Math.floor(d / 30)}mo ago`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const SOURCE_LABELS = {
|
|
193
|
+
@@ -27,6 +40,25 @@ async function generateHTMLReport(analysis, outPath) {
|
|
194
|
+
const { summary, findings, sessions } = analysis;
|
|
195
|
+
const bleed = summary.estimatedMonthlyBleed;
|
|
196
|
+
|
|
197
|
+
+ // Burn summary calculation
|
|
198
|
+
+ const activeSessions = (sessions || []).filter(s => s.dailyCost);
|
|
199
|
+
+ const totalDaily = activeSessions.reduce((sum, s) => sum + (s.dailyCost || 0), 0);
|
|
200
|
+
+ const totalMonthly = totalDaily * 30;
|
|
201
|
+
+ const burnSummary = totalDaily > 0 ? `
|
|
202
|
+
+ <div class="section">
|
|
203
|
+
+ <div class="section-title">Session Burn Rate</div>
|
|
204
|
+
+ <div style="display:flex; gap:32px; flex-wrap:wrap;">
|
|
205
|
+
+ <div>
|
|
206
|
+
+ <div style="font-size:24px; font-weight:800; color:#f59e0b;">$${totalDaily.toFixed(4)}/day</div>
|
|
207
|
+
+ <div style="font-size:13px; color:#94a3b8; margin-top:4px;">Combined daily burn rate across ${activeSessions.length} active session${activeSessions.length !== 1 ? 's' : ''}</div>
|
|
208
|
+
+ </div>
|
|
209
|
+
+ <div>
|
|
210
|
+
+ <div style="font-size:24px; font-weight:800; color:#f97316;">$${totalMonthly.toFixed(2)}/month</div>
|
|
211
|
+
+ <div style="font-size:13px; color:#94a3b8; margin-top:4px;">Projected monthly from active sessions</div>
|
|
212
|
+
+ </div>
|
|
213
|
+
+ </div>
|
|
214
|
+
+ </div>` : '';
|
|
215
|
+
+
|
|
216
|
+
const findingCards = findings.map(f => `
|
|
217
|
+
<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;">
|
|
218
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
|
|
219
|
+
@@ -44,14 +76,22 @@ async function generateHTMLReport(analysis, outPath) {
|
|
220
|
+
const sessionRows = (sessions || [])
|
|
221
|
+
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
222
|
+
.slice(0, 20)
|
|
223
|
+
- .map(s => `
|
|
224
|
+
+ .map(s => {
|
|
225
|
+
+ const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
226
|
+
+ const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
227
|
+
+ const age = s.ageMs ? relativeAge(s.ageMs) : 'unknown';
|
|
228
|
+
+ const absDate = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '';
|
|
229
|
+
+ const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}/day` : 'โ';
|
|
230
|
+
+ return `
|
|
231
|
+
<tr style="${s.isOrphaned ? 'background:#fff7ed' : ''}">
|
|
232
|
+
- <td style="padding:8px 12px; font-family:monospace; font-size:13px">${s.key}${s.isOrphaned ? ' โ ๏ธ' : ''}</td>
|
|
233
|
+
+ <td style="padding:8px 12px; font-family:monospace; font-size:13px">${keyDisplay}${flag}</td>
|
|
234
|
+
<td style="padding:8px 12px">${s.modelLabel || s.model}</td>
|
|
235
|
+
<td style="padding:8px 12px; text-align:right">${(s.inputTokens + s.outputTokens).toLocaleString()}</td>
|
|
236
|
+
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(6)}</td>
|
|
237
|
+
+ <td style="padding:8px 12px; text-align:right; color:#f59e0b">${daily}</td>
|
|
238
|
+
+ <td style="padding:8px 12px; color:#6b7280; font-size:13px" title="${absDate}">${age}</td>
|
|
239
|
+
</tr>
|
|
240
|
+
- `).join('');
|
|
241
|
+
+ `}).join('');
|
|
242
|
+
|
|
243
|
+
const html = `<!DOCTYPE html>
|
|
244
|
+
<html lang="en">
|
|
245
|
+
@@ -66,7 +106,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
246
|
+
.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; }
|
|
247
|
+
.tagline { color: #94a3b8; margin-top: 8px; font-size: 16px; }
|
|
248
|
+
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
249
|
+
- .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
250
|
+
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
251
|
+
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
252
|
+
.card-value { font-size: 32px; font-weight: 800; }
|
|
253
|
+
.card-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
|
254
|
+
@@ -111,6 +151,10 @@ async function generateHTMLReport(analysis, outPath) {
|
|
255
|
+
<div class="card-value" style="color:#eab308">${summary.medium}</div>
|
|
256
|
+
<div class="card-label">๐ก Medium</div>
|
|
257
|
+
</div>
|
|
258
|
+
+ <div class="card">
|
|
259
|
+
+ <div class="card-value" style="color:#06b6d4">${summary.low || 0}</div>
|
|
260
|
+
+ <div class="card-label">๐ต Low</div>
|
|
261
|
+
+ </div>
|
|
262
|
+
<div class="card">
|
|
263
|
+
<div class="card-value" style="color:#22c55e">${summary.info}</div>
|
|
264
|
+
<div class="card-label">โ
OK</div>
|
|
265
|
+
@@ -137,11 +181,12 @@ async function generateHTMLReport(analysis, outPath) {
|
|
266
|
+
<div class="section-title">Session Breakdown</div>
|
|
267
|
+
<div style="overflow-x:auto">
|
|
268
|
+
<table>
|
|
269
|
+
- <thead><tr><th>Session</th><th>Model</th><th style="text-align:right">Tokens</th><th style="text-align:right">Cost</th></tr></thead>
|
|
270
|
+
+ <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>
|
|
271
|
+
<tbody>${sessionRows}</tbody>
|
|
272
|
+
</table>
|
|
273
|
+
</div>
|
|
274
|
+
- </div>` : ''}
|
|
275
|
+
+ </div>
|
|
276
|
+
+ ${burnSummary}` : ''}
|
|
277
|
+
|
|
278
|
+
</div>
|
|
279
|
+
<div class="footer">
|
|
280
|
+
@@ -151,7 +196,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
281
|
+
</html>`;
|
|
282
|
+
|
|
283
|
+
if (!outPath) {
|
|
284
|
+
- outPath = path.join(os.tmpdir(), `clawculator-report-${Date.now()}.html`);
|
|
285
|
+
+ outPath = path.join(process.cwd(), `clawculator-report-${Date.now()}.html`);
|
|
286
|
+
}
|
|
287
|
+
fs.writeFileSync(outPath, html, 'utf8');
|
|
288
|
+
return outPath;
|
|
289
|
+
diff --git a/src/mdReport.js b/src/mdReport.js
|
|
290
|
+
index 630818e..7a0a839 100644
|
|
291
|
+
--- a/src/mdReport.js
|
|
292
|
+
+++ b/src/mdReport.js
|
|
293
|
+
@@ -1,6 +1,6 @@
|
|
294
|
+
'use strict';
|
|
295
|
+
|
|
296
|
+
-const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' };
|
|
297
|
+
+const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' };
|
|
298
|
+
const SOURCE_LABELS = {
|
|
299
|
+
heartbeat: '๐ Heartbeat', hooks: '๐ช Hooks', whatsapp: '๐ฑ WhatsApp',
|
|
300
|
+
subagents: '๐ค Subagents', skills: '๐ง Skills', memory: '๐ง Memory',
|
|
301
|
+
@@ -8,6 +8,25 @@ const SOURCE_LABELS = {
|
|
302
|
+
workspace: '๐ Workspace', config: '๐ Config',
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
+function relativeAge(ageMs) {
|
|
306
|
+
+ if (!ageMs) return 'unknown';
|
|
307
|
+
+ const s = Math.floor(ageMs / 1000);
|
|
308
|
+
+ if (s < 60) return `${s}s ago`;
|
|
309
|
+
+ const m = Math.floor(s / 60);
|
|
310
|
+
+ if (m < 60) return `${m}m ago`;
|
|
311
|
+
+ const h = Math.floor(m / 60);
|
|
312
|
+
+ if (h < 24) return `${h}h ago`;
|
|
313
|
+
+ const d = Math.floor(h / 24);
|
|
314
|
+
+ if (d < 30) return `${d}d ago`;
|
|
315
|
+
+ const mo = Math.floor(d / 30);
|
|
316
|
+
+ return `${mo}mo ago`;
|
|
317
|
+
+}
|
|
318
|
+
+
|
|
319
|
+
+function absoluteDate(updatedAt) {
|
|
320
|
+
+ if (!updatedAt) return '';
|
|
321
|
+
+ return new Date(updatedAt).toLocaleString();
|
|
322
|
+
+}
|
|
323
|
+
+
|
|
324
|
+
function generateMarkdownReport(analysis) {
|
|
325
|
+
const { summary, findings, sessions, primaryModel, scannedAt } = analysis;
|
|
326
|
+
const bleed = summary.estimatedMonthlyBleed;
|
|
327
|
+
@@ -32,21 +51,31 @@ function generateMarkdownReport(analysis) {
|
|
328
|
+
// Summary table
|
|
329
|
+
lines.push('## Summary');
|
|
330
|
+
lines.push('');
|
|
331
|
+
- lines.push('| Severity | Count |');
|
|
332
|
+
- lines.push('|----------|-------|');
|
|
333
|
+
+ lines.push('| Metric | Value |');
|
|
334
|
+
+ lines.push('|--------|-------|');
|
|
335
|
+
lines.push(`| ๐ด Critical | ${summary.critical} |`);
|
|
336
|
+
lines.push(`| ๐ High | ${summary.high} |`);
|
|
337
|
+
lines.push(`| ๐ก Medium | ${summary.medium} |`);
|
|
338
|
+
+ lines.push(`| ๐ต Low | ${summary.low || 0} |`);
|
|
339
|
+
lines.push(`| โ
OK | ${summary.info} |`);
|
|
340
|
+
lines.push(`| Sessions analyzed | ${summary.sessionsAnalyzed} |`);
|
|
341
|
+
lines.push(`| Total tokens found | ${(summary.totalTokensFound || 0).toLocaleString()} |`);
|
|
342
|
+
+
|
|
343
|
+
+ // Session cost summary
|
|
344
|
+
+ if (sessions?.length > 0) {
|
|
345
|
+
+ const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
346
|
+
+ const dailyRates = sessions.filter(s => s.dailyCost).map(s => s.dailyCost);
|
|
347
|
+
+ const totalDailyRate = dailyRates.reduce((sum, r) => sum + r, 0);
|
|
348
|
+
+ if (totalCost > 0) lines.push(`| Total session cost (lifetime) | $${totalCost.toFixed(4)} |`);
|
|
349
|
+
+ if (totalDailyRate > 0) lines.push(`| Avg daily burn rate | $${totalDailyRate.toFixed(4)}/day (~$${(totalDailyRate * 30).toFixed(2)}/month) |`);
|
|
350
|
+
+ }
|
|
351
|
+
lines.push('');
|
|
352
|
+
|
|
353
|
+
// Findings by severity
|
|
354
|
+
lines.push('## Findings');
|
|
355
|
+
lines.push('');
|
|
356
|
+
|
|
357
|
+
- for (const severity of ['critical', 'high', 'medium', 'info']) {
|
|
358
|
+
+ for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
359
|
+
const group = findings.filter(f => f.severity === severity);
|
|
360
|
+
if (!group.length) continue;
|
|
361
|
+
|
|
362
|
+
@@ -78,19 +107,24 @@ function generateMarkdownReport(analysis) {
|
|
363
|
+
if (sessions?.length > 0) {
|
|
364
|
+
lines.push('## Session Breakdown');
|
|
365
|
+
lines.push('');
|
|
366
|
+
- lines.push('| Session | Model | Tokens | Cost |');
|
|
367
|
+
- lines.push('|---------|-------|--------|------|');
|
|
368
|
+
+ lines.push('| Session | Model | Tokens | Total Cost | $/day | Last Active |');
|
|
369
|
+
+ lines.push('|---------|-------|--------|-----------|-------|-------------|');
|
|
370
|
+
|
|
371
|
+
[...sessions]
|
|
372
|
+
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
373
|
+
.slice(0, 20)
|
|
374
|
+
.forEach(s => {
|
|
375
|
+
- const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
376
|
+
- const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
377
|
+
- lines.push(`| \`${s.key}${flag}\` | ${s.modelLabel || s.model} | ${tok} | $${s.cost.toFixed(6)} |`);
|
|
378
|
+
+ const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
379
|
+
+ const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
380
|
+
+ const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
381
|
+
+ const age = s.ageMs ? `${relativeAge(s.ageMs)} (${absoluteDate(s.updatedAt)})` : 'unknown';
|
|
382
|
+
+ const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ';
|
|
383
|
+
+ lines.push(`| \`${keyDisplay}${flag}\` | ${s.modelLabel || s.model} | ${tok} | $${s.cost.toFixed(6)} | ${daily} | ${age} |`);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
lines.push('');
|
|
387
|
+
+ lines.push('> โ ๏ธ = orphaned session ยท $/day = total cost รท session age');
|
|
388
|
+
+ lines.push('');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Quick wins
|
|
392
|
+
diff --git a/src/reporter.js b/src/reporter.js
|
|
393
|
+
index d850c4a..8e35519 100644
|
|
394
|
+
--- a/src/reporter.js
|
|
395
|
+
+++ b/src/reporter.js
|
|
396
|
+
@@ -36,6 +36,19 @@ function formatCost(cost) {
|
|
397
|
+
return `\x1b[31m$${cost.toFixed(2)}/mo\x1b[0m`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
+function relativeAge(ageMs) {
|
|
401
|
+
+ if (!ageMs) return 'unknown';
|
|
402
|
+
+ const s = Math.floor(ageMs / 1000);
|
|
403
|
+
+ if (s < 60) return `${s}s ago`;
|
|
404
|
+
+ const m = Math.floor(s / 60);
|
|
405
|
+
+ if (m < 60) return `${m}m ago`;
|
|
406
|
+
+ const h = Math.floor(m / 60);
|
|
407
|
+
+ if (h < 24) return `${h}h ago`;
|
|
408
|
+
+ const d = Math.floor(h / 24);
|
|
409
|
+
+ if (d < 30) return `${d}d ago`;
|
|
410
|
+
+ return `${Math.floor(d / 30)}mo ago`;
|
|
411
|
+
+}
|
|
412
|
+
+
|
|
413
|
+
function generateTerminalReport(analysis) {
|
|
414
|
+
const { summary, findings, sessions } = analysis;
|
|
415
|
+
const R = '\x1b[0m';
|
|
416
|
+
@@ -78,12 +91,22 @@ function generateTerminalReport(analysis) {
|
|
417
|
+
if (sessions?.length > 0) {
|
|
418
|
+
console.log(`${C}โโโ Top Sessions by Token Usage โโโ${R}\n`);
|
|
419
|
+
const sorted = [...sessions].sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens)).slice(0, 8);
|
|
420
|
+
- console.log(` ${D}${'Session'.padEnd(42)} ${'Model'.padEnd(22)} ${'Tokens'.padEnd(10)} Cost${R}`);
|
|
421
|
+
- console.log(` ${D}${'โ'.repeat(85)}${R}`);
|
|
422
|
+
+ console.log(` ${D}${'Session'.padEnd(16)} ${'Model'.padEnd(20)} ${'Tokens'.padEnd(10)} ${'Total Cost'.padEnd(12)} ${'$/day'.padEnd(10)} Last Active${R}`);
|
|
423
|
+
+ console.log(` ${D}${'โ'.repeat(95)}${R}`);
|
|
424
|
+
for (const s of sorted) {
|
|
425
|
+
- const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
426
|
+
- const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
427
|
+
- console.log(` ${(s.key + flag).slice(0, 42).padEnd(42)} ${(s.modelLabel || s.model || 'unknown').slice(0, 22).padEnd(22)} ${tok.padEnd(10)} $${s.cost.toFixed(6)}`);
|
|
428
|
+
+ const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
429
|
+
+ const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
430
|
+
+ const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
431
|
+
+ const age = s.ageMs ? `${relativeAge(s.ageMs)} (${new Date(s.updatedAt).toLocaleDateString()})` : 'unknown';
|
|
432
|
+
+ const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ';
|
|
433
|
+
+ 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}`);
|
|
434
|
+
+ }
|
|
435
|
+
+
|
|
436
|
+
+ // Daily burn rate summary
|
|
437
|
+
+ const totalDailyRate = sessions.filter(s => s.dailyCost).reduce((sum, s) => sum + s.dailyCost, 0);
|
|
438
|
+
+ if (totalDailyRate > 0) {
|
|
439
|
+
+ console.log();
|
|
440
|
+
+ console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D} ยท ~$${(totalDailyRate * 30).toFixed(2)}/month${R}`);
|
|
441
|
+
}
|
|
442
|
+
console.log();
|
|
443
|
+
}
|
|
444
|
+
@@ -93,7 +116,7 @@ function generateTerminalReport(analysis) {
|
|
445
|
+
console.log(` ๐ด ${RED}${summary.critical}${R} critical ๐ ${summary.high} high ๐ก ${summary.medium} medium ๐ต ${summary.low||0} low โ
${summary.info} ok`);
|
|
446
|
+
console.log(` Sessions analyzed: ${summary.sessionsAnalyzed} ยท Tokens found: ${(summary.totalTokensFound||0).toLocaleString()}`);
|
|
447
|
+
if (bleed > 0) {
|
|
448
|
+
- console.log(` ${RED}${B}Monthly bleed: $${bleed.toFixed(2)}/month${R}`);
|
|
449
|
+
+ console.log(` ${RED}${B}Estimated monthly bleed: $${bleed.toFixed(2)}/month${R}`);
|
|
450
|
+
} else {
|
|
451
|
+
console.log(` ${GRN}No significant cost bleed detected โ${R}`);
|
|
452
|
+
}
|
package/package.json
CHANGED
|
@@ -5,15 +5,15 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
7
|
function severityColor(severity) {
|
|
8
|
-
return { critical: '#ef4444', high: '#f97316', medium: '#eab308', info: '#22c55e' }[severity] || '#6b7280';
|
|
8
|
+
return { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#06b6d4', info: '#22c55e' }[severity] || '#6b7280';
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function severityBg(severity) {
|
|
12
|
-
return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
12
|
+
return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', low: '#ecfeff', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function severityIcon(severity) {
|
|
16
|
-
return { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' }[severity] || 'โช';
|
|
16
|
+
return { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' }[severity] || 'โช';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function relativeAge(ageMs) {
|
|
@@ -40,6 +40,25 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
40
40
|
const { summary, findings, sessions } = analysis;
|
|
41
41
|
const bleed = summary.estimatedMonthlyBleed;
|
|
42
42
|
|
|
43
|
+
// Burn summary calculation
|
|
44
|
+
const activeSessions = (sessions || []).filter(s => s.dailyCost);
|
|
45
|
+
const totalDaily = activeSessions.reduce((sum, s) => sum + (s.dailyCost || 0), 0);
|
|
46
|
+
const totalMonthly = totalDaily * 30;
|
|
47
|
+
const burnSummary = totalDaily > 0 ? `
|
|
48
|
+
<div class="section">
|
|
49
|
+
<div class="section-title">Session Burn Rate</div>
|
|
50
|
+
<div style="display:flex; gap:32px; flex-wrap:wrap;">
|
|
51
|
+
<div>
|
|
52
|
+
<div style="font-size:24px; font-weight:800; color:#f59e0b;">$${totalDaily.toFixed(4)}/day</div>
|
|
53
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Combined daily burn rate across ${activeSessions.length} active session${activeSessions.length !== 1 ? 's' : ''}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<div style="font-size:24px; font-weight:800; color:#f97316;">$${totalMonthly.toFixed(2)}/month</div>
|
|
57
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Projected monthly from active sessions</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>` : '';
|
|
61
|
+
|
|
43
62
|
const findingCards = findings.map(f => `
|
|
44
63
|
<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
64
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
|
|
@@ -87,7 +106,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
87
106
|
.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; }
|
|
88
107
|
.tagline { color: #94a3b8; margin-top: 8px; font-size: 16px; }
|
|
89
108
|
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
90
|
-
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(
|
|
109
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
91
110
|
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
92
111
|
.card-value { font-size: 32px; font-weight: 800; }
|
|
93
112
|
.card-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
|
@@ -132,6 +151,10 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
132
151
|
<div class="card-value" style="color:#eab308">${summary.medium}</div>
|
|
133
152
|
<div class="card-label">๐ก Medium</div>
|
|
134
153
|
</div>
|
|
154
|
+
<div class="card">
|
|
155
|
+
<div class="card-value" style="color:#06b6d4">${summary.low || 0}</div>
|
|
156
|
+
<div class="card-label">๐ต Low</div>
|
|
157
|
+
</div>
|
|
135
158
|
<div class="card">
|
|
136
159
|
<div class="card-value" style="color:#22c55e">${summary.info}</div>
|
|
137
160
|
<div class="card-label">โ
OK</div>
|
|
@@ -162,7 +185,8 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
162
185
|
<tbody>${sessionRows}</tbody>
|
|
163
186
|
</table>
|
|
164
187
|
</div>
|
|
165
|
-
</div
|
|
188
|
+
</div>
|
|
189
|
+
${burnSummary}` : ''}
|
|
166
190
|
|
|
167
191
|
</div>
|
|
168
192
|
<div class="footer">
|
|
@@ -172,7 +196,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
172
196
|
</html>`;
|
|
173
197
|
|
|
174
198
|
if (!outPath) {
|
|
175
|
-
outPath = path.join(
|
|
199
|
+
outPath = path.join(process.cwd(), `clawculator-report-${Date.now()}.html`);
|
|
176
200
|
}
|
|
177
201
|
fs.writeFileSync(outPath, html, 'utf8');
|
|
178
202
|
return outPath;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' };
|
|
3
|
+
const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' };
|
|
4
4
|
const SOURCE_LABELS = {
|
|
5
5
|
heartbeat: '๐ Heartbeat', hooks: '๐ช Hooks', whatsapp: '๐ฑ WhatsApp',
|
|
6
6
|
subagents: '๐ค Subagents', skills: '๐ง Skills', memory: '๐ง Memory',
|
|
@@ -56,6 +56,7 @@ function generateMarkdownReport(analysis) {
|
|
|
56
56
|
lines.push(`| ๐ด Critical | ${summary.critical} |`);
|
|
57
57
|
lines.push(`| ๐ High | ${summary.high} |`);
|
|
58
58
|
lines.push(`| ๐ก Medium | ${summary.medium} |`);
|
|
59
|
+
lines.push(`| ๐ต Low | ${summary.low || 0} |`);
|
|
59
60
|
lines.push(`| โ
OK | ${summary.info} |`);
|
|
60
61
|
lines.push(`| Sessions analyzed | ${summary.sessionsAnalyzed} |`);
|
|
61
62
|
lines.push(`| Total tokens found | ${(summary.totalTokensFound || 0).toLocaleString()} |`);
|
|
@@ -74,7 +75,7 @@ function generateMarkdownReport(analysis) {
|
|
|
74
75
|
lines.push('## Findings');
|
|
75
76
|
lines.push('');
|
|
76
77
|
|
|
77
|
-
for (const severity of ['critical', 'high', 'medium', 'info']) {
|
|
78
|
+
for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
78
79
|
const group = findings.filter(f => f.severity === severity);
|
|
79
80
|
if (!group.length) continue;
|
|
80
81
|
|
package/src/analyzer.js
CHANGED
|
@@ -141,6 +141,16 @@ function isLocalModel(modelStr) {
|
|
|
141
141
|
['qwen', 'llama', 'mistral', 'phi', 'gemma', 'deepseek', 'kimi'].some(m => lower.includes(m));
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function isHaikuTier(modelStr) {
|
|
145
|
+
if (!modelStr) return false;
|
|
146
|
+
const lower = modelStr.toLowerCase();
|
|
147
|
+
return lower.includes('haiku');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isAcceptableForHooks(modelStr) {
|
|
151
|
+
return isLocalModel(modelStr) || isHaikuTier(modelStr);
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
function isOpenRouter(modelStr) {
|
|
145
155
|
return modelStr?.toLowerCase().startsWith('openrouter/');
|
|
146
156
|
}
|
|
@@ -337,25 +347,37 @@ function analyzeConfig(configPath) {
|
|
|
337
347
|
const hooks = config.hooks?.internal?.entries || config.hooks || {};
|
|
338
348
|
const hookNames = Object.keys(hooks).filter(k => k !== 'enabled' && k !== 'token' && k !== 'path');
|
|
339
349
|
let hookIssues = 0;
|
|
350
|
+
let haikuHooks = 0;
|
|
340
351
|
|
|
341
352
|
for (const name of hookNames) {
|
|
342
353
|
const hook = typeof hooks[name] === 'object' ? hooks[name] : {};
|
|
343
354
|
if (hook.enabled === false) continue;
|
|
344
355
|
const hookModel = hook.model || primaryModel;
|
|
345
|
-
if (
|
|
356
|
+
if (isLocalModel(hookModel)) {
|
|
357
|
+
// local = free, no finding needed
|
|
358
|
+
} else if (isHaikuTier(hookModel) && resolveModel(hookModel)) {
|
|
359
|
+
const monthly = costPerCall(resolveModel(hookModel), 1000, 200) * 50 * 30;
|
|
360
|
+
haikuHooks++;
|
|
361
|
+
findings.push({
|
|
362
|
+
severity: 'low', source: 'hooks',
|
|
363
|
+
message: `Hook "${name}" on Haiku โ minimal cost, good choice`,
|
|
364
|
+
detail: `~50 fires/day estimated ยท $${monthly.toFixed(2)}/month`,
|
|
365
|
+
monthlyCost: monthly,
|
|
366
|
+
});
|
|
367
|
+
} else if (resolveModel(hookModel)) {
|
|
346
368
|
const monthly = costPerCall(resolveModel(hookModel), 1000, 200) * 50 * 30;
|
|
347
369
|
hookIssues++;
|
|
348
370
|
findings.push({
|
|
349
371
|
severity: 'high', source: 'hooks',
|
|
350
|
-
message: `Hook "${name}" running on
|
|
372
|
+
message: `Hook "${name}" running on ${hookModel} โ switch to Haiku or local`,
|
|
351
373
|
detail: `~50 fires/day estimated ยท $${monthly.toFixed(2)}/month`,
|
|
352
374
|
monthlyCost: monthly,
|
|
353
375
|
...FIXES.HOOK_PAID_MODEL(name),
|
|
354
376
|
});
|
|
355
377
|
}
|
|
356
378
|
}
|
|
357
|
-
if (hookNames.length > 0 && hookIssues === 0) {
|
|
358
|
-
findings.push({ severity: 'info', source: 'hooks', message: `All ${hookNames.length} hooks on
|
|
379
|
+
if (hookNames.length > 0 && hookIssues === 0 && haikuHooks === 0) {
|
|
380
|
+
findings.push({ severity: 'info', source: 'hooks', message: `All ${hookNames.length} hooks on local models โ`, monthlyCost: 0 });
|
|
359
381
|
}
|
|
360
382
|
|
|
361
383
|
// โโ WhatsApp โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -506,7 +528,11 @@ function analyzeSessions(sessionsPath) {
|
|
|
506
528
|
if (isOrphaned) orphaned.push({ key, model, tokens: inTok + outTok, cost });
|
|
507
529
|
if (inTok + outTok > 50000) large.push({ key, model, tokens: inTok + outTok });
|
|
508
530
|
|
|
509
|
-
|
|
531
|
+
const ageMs = updatedAt ? Date.now() - new Date(updatedAt).getTime() : null;
|
|
532
|
+
const ageDays = ageMs ? ageMs / (1000 * 3600 * 24) : null;
|
|
533
|
+
const dailyCost = (ageDays && ageDays > 0.01 && cost > 0) ? cost / ageDays : null;
|
|
534
|
+
|
|
535
|
+
breakdown.push({ key, model, modelLabel: modelKey ? (MODEL_PRICING[modelKey]?.label || modelKey) : 'unknown', inputTokens: inTok, outputTokens: outTok, cost, updatedAt, ageMs, dailyCost, isOrphaned });
|
|
510
536
|
}
|
|
511
537
|
|
|
512
538
|
if (orphaned.length > 0) findings.push({ severity: 'high', source: 'sessions', message: `${orphaned.length} orphaned session(s) โ still holding tokens on paid models`, detail: orphaned.map(s => `${s.key}: ${s.tokens.toLocaleString()} tokens ($${s.cost.toFixed(4)})`).join('\n '), ...FIXES.ORPHANED_SESSIONS });
|
|
@@ -558,6 +584,7 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
|
558
584
|
critical: allFindings.filter(f => f.severity === 'critical').length,
|
|
559
585
|
high: allFindings.filter(f => f.severity === 'high').length,
|
|
560
586
|
medium: allFindings.filter(f => f.severity === 'medium').length,
|
|
587
|
+
low: allFindings.filter(f => f.severity === 'low').length,
|
|
561
588
|
info: allFindings.filter(f => f.severity === 'info').length,
|
|
562
589
|
estimatedMonthlyBleed,
|
|
563
590
|
sessionsAnalyzed: sessionResult.sessionCount,
|
package/src/htmlReport.js
CHANGED
|
@@ -5,15 +5,28 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
7
|
function severityColor(severity) {
|
|
8
|
-
return { critical: '#ef4444', high: '#f97316', medium: '#eab308', info: '#22c55e' }[severity] || '#6b7280';
|
|
8
|
+
return { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#06b6d4', info: '#22c55e' }[severity] || '#6b7280';
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function severityBg(severity) {
|
|
12
|
-
return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
12
|
+
return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', low: '#ecfeff', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function severityIcon(severity) {
|
|
16
|
-
return { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' }[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`;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
const SOURCE_LABELS = {
|
|
@@ -27,6 +40,25 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
27
40
|
const { summary, findings, sessions } = analysis;
|
|
28
41
|
const bleed = summary.estimatedMonthlyBleed;
|
|
29
42
|
|
|
43
|
+
// Burn summary calculation
|
|
44
|
+
const activeSessions = (sessions || []).filter(s => s.dailyCost);
|
|
45
|
+
const totalDaily = activeSessions.reduce((sum, s) => sum + (s.dailyCost || 0), 0);
|
|
46
|
+
const totalMonthly = totalDaily * 30;
|
|
47
|
+
const burnSummary = totalDaily > 0 ? `
|
|
48
|
+
<div class="section">
|
|
49
|
+
<div class="section-title">Session Burn Rate</div>
|
|
50
|
+
<div style="display:flex; gap:32px; flex-wrap:wrap;">
|
|
51
|
+
<div>
|
|
52
|
+
<div style="font-size:24px; font-weight:800; color:#f59e0b;">$${totalDaily.toFixed(4)}/day</div>
|
|
53
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Combined daily burn rate across ${activeSessions.length} active session${activeSessions.length !== 1 ? 's' : ''}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<div style="font-size:24px; font-weight:800; color:#f97316;">$${totalMonthly.toFixed(2)}/month</div>
|
|
57
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Projected monthly from active sessions</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>` : '';
|
|
61
|
+
|
|
30
62
|
const findingCards = findings.map(f => `
|
|
31
63
|
<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;">
|
|
32
64
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
|
|
@@ -44,14 +76,22 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
44
76
|
const sessionRows = (sessions || [])
|
|
45
77
|
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
46
78
|
.slice(0, 20)
|
|
47
|
-
.map(s =>
|
|
79
|
+
.map(s => {
|
|
80
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
81
|
+
const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
82
|
+
const age = s.ageMs ? relativeAge(s.ageMs) : 'unknown';
|
|
83
|
+
const absDate = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '';
|
|
84
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}/day` : 'โ';
|
|
85
|
+
return `
|
|
48
86
|
<tr style="${s.isOrphaned ? 'background:#fff7ed' : ''}">
|
|
49
|
-
<td style="padding:8px 12px; font-family:monospace; font-size:13px">${
|
|
87
|
+
<td style="padding:8px 12px; font-family:monospace; font-size:13px">${keyDisplay}${flag}</td>
|
|
50
88
|
<td style="padding:8px 12px">${s.modelLabel || s.model}</td>
|
|
51
89
|
<td style="padding:8px 12px; text-align:right">${(s.inputTokens + s.outputTokens).toLocaleString()}</td>
|
|
52
90
|
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(6)}</td>
|
|
91
|
+
<td style="padding:8px 12px; text-align:right; color:#f59e0b">${daily}</td>
|
|
92
|
+
<td style="padding:8px 12px; color:#6b7280; font-size:13px" title="${absDate}">${age}</td>
|
|
53
93
|
</tr>
|
|
54
|
-
`).join('');
|
|
94
|
+
`}).join('');
|
|
55
95
|
|
|
56
96
|
const html = `<!DOCTYPE html>
|
|
57
97
|
<html lang="en">
|
|
@@ -66,7 +106,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
66
106
|
.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; }
|
|
67
107
|
.tagline { color: #94a3b8; margin-top: 8px; font-size: 16px; }
|
|
68
108
|
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
69
|
-
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(
|
|
109
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
70
110
|
.card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
71
111
|
.card-value { font-size: 32px; font-weight: 800; }
|
|
72
112
|
.card-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
|
@@ -111,6 +151,10 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
111
151
|
<div class="card-value" style="color:#eab308">${summary.medium}</div>
|
|
112
152
|
<div class="card-label">๐ก Medium</div>
|
|
113
153
|
</div>
|
|
154
|
+
<div class="card">
|
|
155
|
+
<div class="card-value" style="color:#06b6d4">${summary.low || 0}</div>
|
|
156
|
+
<div class="card-label">๐ต Low</div>
|
|
157
|
+
</div>
|
|
114
158
|
<div class="card">
|
|
115
159
|
<div class="card-value" style="color:#22c55e">${summary.info}</div>
|
|
116
160
|
<div class="card-label">โ
OK</div>
|
|
@@ -137,11 +181,12 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
137
181
|
<div class="section-title">Session Breakdown</div>
|
|
138
182
|
<div style="overflow-x:auto">
|
|
139
183
|
<table>
|
|
140
|
-
<thead><tr><th>Session</th><th>Model</th><th style="text-align:right">Tokens</th><th style="text-align:right">Cost</th></tr></thead>
|
|
184
|
+
<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>
|
|
141
185
|
<tbody>${sessionRows}</tbody>
|
|
142
186
|
</table>
|
|
143
187
|
</div>
|
|
144
|
-
</div
|
|
188
|
+
</div>
|
|
189
|
+
${burnSummary}` : ''}
|
|
145
190
|
|
|
146
191
|
</div>
|
|
147
192
|
<div class="footer">
|
|
@@ -151,7 +196,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
151
196
|
</html>`;
|
|
152
197
|
|
|
153
198
|
if (!outPath) {
|
|
154
|
-
outPath = path.join(
|
|
199
|
+
outPath = path.join(process.cwd(), `clawculator-report-${Date.now()}.html`);
|
|
155
200
|
}
|
|
156
201
|
fs.writeFileSync(outPath, html, 'utf8');
|
|
157
202
|
return outPath;
|
package/src/mdReport.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' };
|
|
3
|
+
const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' };
|
|
4
4
|
const SOURCE_LABELS = {
|
|
5
5
|
heartbeat: '๐ Heartbeat', hooks: '๐ช Hooks', whatsapp: '๐ฑ WhatsApp',
|
|
6
6
|
subagents: '๐ค Subagents', skills: '๐ง Skills', memory: '๐ง Memory',
|
|
@@ -8,6 +8,25 @@ const SOURCE_LABELS = {
|
|
|
8
8
|
workspace: '๐ Workspace', config: '๐ Config',
|
|
9
9
|
};
|
|
10
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
|
+
|
|
11
30
|
function generateMarkdownReport(analysis) {
|
|
12
31
|
const { summary, findings, sessions, primaryModel, scannedAt } = analysis;
|
|
13
32
|
const bleed = summary.estimatedMonthlyBleed;
|
|
@@ -32,21 +51,31 @@ function generateMarkdownReport(analysis) {
|
|
|
32
51
|
// Summary table
|
|
33
52
|
lines.push('## Summary');
|
|
34
53
|
lines.push('');
|
|
35
|
-
lines.push('|
|
|
36
|
-
lines.push('
|
|
54
|
+
lines.push('| Metric | Value |');
|
|
55
|
+
lines.push('|--------|-------|');
|
|
37
56
|
lines.push(`| ๐ด Critical | ${summary.critical} |`);
|
|
38
57
|
lines.push(`| ๐ High | ${summary.high} |`);
|
|
39
58
|
lines.push(`| ๐ก Medium | ${summary.medium} |`);
|
|
59
|
+
lines.push(`| ๐ต Low | ${summary.low || 0} |`);
|
|
40
60
|
lines.push(`| โ
OK | ${summary.info} |`);
|
|
41
61
|
lines.push(`| Sessions analyzed | ${summary.sessionsAnalyzed} |`);
|
|
42
62
|
lines.push(`| Total tokens found | ${(summary.totalTokensFound || 0).toLocaleString()} |`);
|
|
63
|
+
|
|
64
|
+
// Session cost summary
|
|
65
|
+
if (sessions?.length > 0) {
|
|
66
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
67
|
+
const dailyRates = sessions.filter(s => s.dailyCost).map(s => s.dailyCost);
|
|
68
|
+
const totalDailyRate = dailyRates.reduce((sum, r) => sum + r, 0);
|
|
69
|
+
if (totalCost > 0) lines.push(`| Total session cost (lifetime) | $${totalCost.toFixed(4)} |`);
|
|
70
|
+
if (totalDailyRate > 0) lines.push(`| Avg daily burn rate | $${totalDailyRate.toFixed(4)}/day (~$${(totalDailyRate * 30).toFixed(2)}/month) |`);
|
|
71
|
+
}
|
|
43
72
|
lines.push('');
|
|
44
73
|
|
|
45
74
|
// Findings by severity
|
|
46
75
|
lines.push('## Findings');
|
|
47
76
|
lines.push('');
|
|
48
77
|
|
|
49
|
-
for (const severity of ['critical', 'high', 'medium', 'info']) {
|
|
78
|
+
for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
50
79
|
const group = findings.filter(f => f.severity === severity);
|
|
51
80
|
if (!group.length) continue;
|
|
52
81
|
|
|
@@ -78,19 +107,24 @@ function generateMarkdownReport(analysis) {
|
|
|
78
107
|
if (sessions?.length > 0) {
|
|
79
108
|
lines.push('## Session Breakdown');
|
|
80
109
|
lines.push('');
|
|
81
|
-
lines.push('| Session | Model | Tokens | Cost |');
|
|
82
|
-
lines.push('
|
|
110
|
+
lines.push('| Session | Model | Tokens | Total Cost | $/day | Last Active |');
|
|
111
|
+
lines.push('|---------|-------|--------|-----------|-------|-------------|');
|
|
83
112
|
|
|
84
113
|
[...sessions]
|
|
85
114
|
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
86
115
|
.slice(0, 20)
|
|
87
116
|
.forEach(s => {
|
|
88
|
-
const tok
|
|
89
|
-
const flag
|
|
90
|
-
|
|
117
|
+
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
118
|
+
const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
119
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
120
|
+
const age = s.ageMs ? `${relativeAge(s.ageMs)} (${absoluteDate(s.updatedAt)})` : 'unknown';
|
|
121
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ';
|
|
122
|
+
lines.push(`| \`${keyDisplay}${flag}\` | ${s.modelLabel || s.model} | ${tok} | $${s.cost.toFixed(6)} | ${daily} | ${age} |`);
|
|
91
123
|
});
|
|
92
124
|
|
|
93
125
|
lines.push('');
|
|
126
|
+
lines.push('> โ ๏ธ = orphaned session ยท $/day = total cost รท session age');
|
|
127
|
+
lines.push('');
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
// Quick wins
|
package/src/reporter.js
CHANGED
|
@@ -4,6 +4,7 @@ const SEVERITY_CONFIG = {
|
|
|
4
4
|
critical: { color: '\x1b[31m', icon: '๐ด', label: 'CRITICAL' },
|
|
5
5
|
high: { color: '\x1b[33m', icon: '๐ ', label: 'HIGH' },
|
|
6
6
|
medium: { color: '\x1b[33m', icon: '๐ก', label: 'MEDIUM' },
|
|
7
|
+
low: { color: '\x1b[36m', icon: '๐ต', label: 'LOW' },
|
|
7
8
|
info: { color: '\x1b[32m', icon: 'โ
', label: 'OK' },
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -35,6 +36,19 @@ function formatCost(cost) {
|
|
|
35
36
|
return `\x1b[31m$${cost.toFixed(2)}/mo\x1b[0m`;
|
|
36
37
|
}
|
|
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
|
+
|
|
38
52
|
function generateTerminalReport(analysis) {
|
|
39
53
|
const { summary, findings, sessions } = analysis;
|
|
40
54
|
const R = '\x1b[0m';
|
|
@@ -54,7 +68,7 @@ function generateTerminalReport(analysis) {
|
|
|
54
68
|
console.log(`${B}${RED}โ ๏ธ Estimated monthly cost exposure: $${bleed.toFixed(2)}/month${R}\n`);
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
for (const severity of ['critical', 'high', 'medium', 'info']) {
|
|
71
|
+
for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
58
72
|
const group = findings.filter(f => f.severity === severity);
|
|
59
73
|
if (!group.length) continue;
|
|
60
74
|
|
|
@@ -77,22 +91,32 @@ function generateTerminalReport(analysis) {
|
|
|
77
91
|
if (sessions?.length > 0) {
|
|
78
92
|
console.log(`${C}โโโ Top Sessions by Token Usage โโโ${R}\n`);
|
|
79
93
|
const sorted = [...sessions].sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens)).slice(0, 8);
|
|
80
|
-
console.log(` ${D}${'Session'.padEnd(
|
|
81
|
-
console.log(` ${D}${'โ'.repeat(
|
|
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}`);
|
|
82
96
|
for (const s of sorted) {
|
|
83
|
-
const tok
|
|
84
|
-
const flag
|
|
85
|
-
|
|
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}`);
|
|
86
110
|
}
|
|
87
111
|
console.log();
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
// Summary
|
|
91
115
|
console.log(`${C}โโโ Summary โโโ${R}`);
|
|
92
|
-
console.log(` ๐ด ${RED}${summary.critical}${R} critical ๐ ${summary.high} high ๐ก ${summary.medium} medium โ
${summary.info} ok`);
|
|
116
|
+
console.log(` ๐ด ${RED}${summary.critical}${R} critical ๐ ${summary.high} high ๐ก ${summary.medium} medium ๐ต ${summary.low||0} low โ
${summary.info} ok`);
|
|
93
117
|
console.log(` Sessions analyzed: ${summary.sessionsAnalyzed} ยท Tokens found: ${(summary.totalTokensFound||0).toLocaleString()}`);
|
|
94
118
|
if (bleed > 0) {
|
|
95
|
-
console.log(` ${RED}${B}
|
|
119
|
+
console.log(` ${RED}${B}Estimated monthly bleed: $${bleed.toFixed(2)}/month${R}`);
|
|
96
120
|
} else {
|
|
97
121
|
console.log(` ${GRN}No significant cost bleed detected โ${R}`);
|
|
98
122
|
}
|