clawculator 2.2.1 → 2.3.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.
- package/package.json +1 -1
- package/skills/clawculator/analyzer.js +43 -9
- package/skills/clawculator/htmlReport.js +53 -19
- package/skills/clawculator/reporter.js +38 -13
- package/src/analyzer.js +43 -9
- package/src/htmlReport.js +53 -19
- package/src/reporter.js +38 -13
package/package.json
CHANGED
|
@@ -528,13 +528,16 @@ function parseTranscript(jsonlPath) {
|
|
|
528
528
|
|
|
529
529
|
// Only assistant messages with usage blocks have cost data
|
|
530
530
|
if (entry.type !== 'message') continue;
|
|
531
|
-
|
|
531
|
+
|
|
532
|
+
// Usage can be at entry.usage (some formats) or entry.message.usage (standard format)
|
|
533
|
+
const u = entry.usage || entry.message?.usage;
|
|
534
|
+
if (!u) continue;
|
|
532
535
|
|
|
533
536
|
messageCount++;
|
|
534
|
-
const u = entry.usage;
|
|
535
537
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
+
// Model can be at entry.model or entry.message.model
|
|
539
|
+
const entryModel = entry.model || entry.message?.model;
|
|
540
|
+
if (entryModel && !model) model = entryModel;
|
|
538
541
|
|
|
539
542
|
input += u.input || 0;
|
|
540
543
|
output += u.output || 0;
|
|
@@ -706,13 +709,19 @@ function analyzeSessions(sessionsPath) {
|
|
|
706
709
|
// Scan for untracked .jsonl files (sessions not in sessions.json)
|
|
707
710
|
const trackedIds = new Set(Object.values(sessions).map(s => s.sessionId).filter(Boolean));
|
|
708
711
|
let untrackedCount = 0, untrackedCost = 0, untrackedTokens = 0;
|
|
712
|
+
let untrackedRecentCount = 0, untrackedRecentCost = 0;
|
|
713
|
+
const NOW = Date.now();
|
|
714
|
+
const TODAY_START = new Date(); TODAY_START.setHours(0, 0, 0, 0);
|
|
715
|
+
const todayMs = TODAY_START.getTime();
|
|
716
|
+
|
|
709
717
|
if (sessionsDir) {
|
|
710
718
|
try {
|
|
711
719
|
for (const file of fs.readdirSync(sessionsDir)) {
|
|
712
720
|
if (!file.endsWith('.jsonl')) continue;
|
|
713
721
|
const fileId = file.replace('.jsonl', '');
|
|
714
722
|
if (trackedIds.has(fileId)) continue; // already counted
|
|
715
|
-
const
|
|
723
|
+
const filePath = path.join(sessionsDir, file);
|
|
724
|
+
const transcript = parseTranscript(filePath);
|
|
716
725
|
if (transcript && transcript.messageCount > 0) {
|
|
717
726
|
untrackedCount++;
|
|
718
727
|
untrackedCost += transcript.totalCost;
|
|
@@ -720,6 +729,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
720
729
|
totalRealCost += transcript.totalCost;
|
|
721
730
|
totalCacheRead += transcript.cacheRead;
|
|
722
731
|
totalCacheWrite += transcript.cacheWrite;
|
|
732
|
+
|
|
733
|
+
// Check if this file was modified today (recent vs historical)
|
|
734
|
+
let fileMtime = null;
|
|
735
|
+
try { fileMtime = fs.statSync(filePath).mtimeMs; } catch {}
|
|
736
|
+
const isRecent = fileMtime && (NOW - fileMtime < 48 * 3600 * 1000);
|
|
737
|
+
const isToday = fileMtime && fileMtime >= todayMs;
|
|
738
|
+
if (isRecent) { untrackedRecentCount++; untrackedRecentCost += transcript.totalCost; }
|
|
739
|
+
|
|
723
740
|
breakdown.push({
|
|
724
741
|
key: `untracked:${fileId.slice(0, 8)}`, sessionId: fileId, model: transcript.model,
|
|
725
742
|
modelLabel: transcript.model ? (MODEL_PRICING[resolveModel(transcript.model)]?.label || transcript.model) : 'unknown',
|
|
@@ -729,8 +746,9 @@ function analyzeSessions(sessionsPath) {
|
|
|
729
746
|
hasTranscript: true, isSharedSession: false,
|
|
730
747
|
messageCount: transcript.messageCount,
|
|
731
748
|
updatedAt: transcript.lastTs ? new Date(transcript.lastTs).toISOString() : null,
|
|
732
|
-
ageMs: transcript.lastTs ?
|
|
749
|
+
ageMs: transcript.lastTs ? NOW - transcript.lastTs : null,
|
|
733
750
|
dailyCost: null, isOrphaned: false, isUntracked: true,
|
|
751
|
+
isRecent, isToday,
|
|
734
752
|
});
|
|
735
753
|
}
|
|
736
754
|
}
|
|
@@ -738,11 +756,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
738
756
|
}
|
|
739
757
|
|
|
740
758
|
if (untrackedCount > 0) {
|
|
759
|
+
const detail = [`${untrackedTokens.toLocaleString()} tokens · $${untrackedCost.toFixed(4)} total API costs`];
|
|
760
|
+
if (untrackedRecentCount > 0) detail.push(`${untrackedRecentCount} active in last 48h ($${untrackedRecentCost.toFixed(4)})`);
|
|
761
|
+
detail.push('These are sessions not listed in sessions.json — old/deleted or spawned by subprocesses');
|
|
741
762
|
findings.push({
|
|
742
|
-
severity:
|
|
763
|
+
severity: untrackedRecentCost > 1 ? 'high' : (untrackedCost > 1 ? 'medium' : 'info'),
|
|
743
764
|
source: 'sessions',
|
|
744
|
-
message: `${untrackedCount} untracked session(s) found
|
|
745
|
-
detail:
|
|
765
|
+
message: `${untrackedCount} untracked session(s) found (${untrackedRecentCount} recent)`,
|
|
766
|
+
detail: detail.join('\n'),
|
|
746
767
|
});
|
|
747
768
|
}
|
|
748
769
|
|
|
@@ -791,10 +812,22 @@ function analyzeSessions(sessionsPath) {
|
|
|
791
812
|
}
|
|
792
813
|
}
|
|
793
814
|
|
|
815
|
+
// Compute today's cost across all sessions (tracked + untracked)
|
|
816
|
+
let todayCost = 0;
|
|
817
|
+
for (const s of breakdown) {
|
|
818
|
+
if (s.isToday) { todayCost += s.cost; continue; }
|
|
819
|
+
// For tracked sessions, check if updatedAt is today
|
|
820
|
+
if (!s.isUntracked && s.updatedAt) {
|
|
821
|
+
const upd = new Date(s.updatedAt).getTime();
|
|
822
|
+
if (upd >= todayMs) todayCost += s.cost;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
794
826
|
return {
|
|
795
827
|
exists: true, findings, sessions: breakdown,
|
|
796
828
|
totalInputTokens: totalIn, totalOutputTokens: totalOut,
|
|
797
829
|
totalCost, totalCacheRead, totalCacheWrite, totalRealCost,
|
|
830
|
+
todayCost,
|
|
798
831
|
sessionCount: Object.keys(sessions).length,
|
|
799
832
|
};
|
|
800
833
|
}
|
|
@@ -902,6 +935,7 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
|
902
935
|
totalCacheWrite: sessionResult.totalCacheWrite || 0,
|
|
903
936
|
totalRealCost: realCost,
|
|
904
937
|
totalEstimatedCost: sessionResult.totalCost || 0,
|
|
938
|
+
todayCost: sessionResult.todayCost || 0,
|
|
905
939
|
},
|
|
906
940
|
sessions: sessionResult.sessions || [],
|
|
907
941
|
webChatSessions,
|
|
@@ -40,9 +40,9 @@ 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
|
|
45
|
-
const totalDaily =
|
|
43
|
+
// Burn summary calculation — only tracked sessions (not historical untracked)
|
|
44
|
+
const trackedSessions = (sessions || []).filter(s => !s.isUntracked && s.dailyCost);
|
|
45
|
+
const totalDaily = trackedSessions.reduce((sum, s) => sum + (s.dailyCost || 0), 0);
|
|
46
46
|
const totalMonthly = totalDaily * 30;
|
|
47
47
|
const burnSummary = totalDaily > 0 ? `
|
|
48
48
|
<div class="section">
|
|
@@ -50,7 +50,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
50
50
|
<div style="display:flex; gap:32px; flex-wrap:wrap;">
|
|
51
51
|
<div>
|
|
52
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 ${
|
|
53
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Combined daily burn rate across ${trackedSessions.length} active session${trackedSessions.length !== 1 ? 's' : ''}</div>
|
|
54
54
|
</div>
|
|
55
55
|
<div>
|
|
56
56
|
<div style="font-size:24px; font-weight:800; color:#f97316;">$${totalMonthly.toFixed(2)}/month</div>
|
|
@@ -73,10 +73,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
73
73
|
</div>
|
|
74
74
|
`).join('');
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
78
|
-
.slice(0, 20)
|
|
79
|
-
.map(s => {
|
|
76
|
+
const makeSessionRow = (s) => {
|
|
80
77
|
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '…' : s.key;
|
|
81
78
|
const flag = s.isOrphaned ? ' ⚠️' : '';
|
|
82
79
|
const txBadge = s.hasTranscript ? '<span style="color:#22c55e; font-size:10px; margin-left:4px" title="Cost from .jsonl transcript (API-reported)">●</span>' : '<span style="color:#6b7280; font-size:10px; margin-left:4px" title="Estimated cost (no transcript found)">○</span>';
|
|
@@ -84,16 +81,33 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
84
81
|
const absDate = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '';
|
|
85
82
|
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}/day` : '—';
|
|
86
83
|
const cacheTitle = (s.cacheRead || s.cacheWrite) ? `Cache R: ${(s.cacheRead||0).toLocaleString()} · W: ${(s.cacheWrite||0).toLocaleString()}` : '';
|
|
84
|
+
const rowBg = s.isOrphaned ? 'background:#fff7ed' : (s.isRecent ? 'background:#fffbeb' : '');
|
|
87
85
|
return `
|
|
88
|
-
<tr style="${
|
|
86
|
+
<tr style="${rowBg}">
|
|
89
87
|
<td style="padding:8px 12px; font-family:monospace; font-size:13px">${keyDisplay}${flag}${txBadge}</td>
|
|
90
88
|
<td style="padding:8px 12px">${s.modelLabel || s.model}</td>
|
|
91
89
|
<td style="padding:8px 12px; text-align:right" title="${cacheTitle}">${(s.inputTokens + s.outputTokens).toLocaleString()}${(s.cacheRead || s.cacheWrite) ? `<span style="color:#6b7280; font-size:11px;"> +${((s.cacheRead||0)+(s.cacheWrite||0)).toLocaleString()} cache</span>` : ''}</td>
|
|
92
|
-
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(
|
|
90
|
+
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(4)}</td>
|
|
93
91
|
<td style="padding:8px 12px; text-align:right; color:#f59e0b">${daily}</td>
|
|
94
92
|
<td style="padding:8px 12px; color:#6b7280; font-size:13px" title="${absDate}">${age}</td>
|
|
95
|
-
</tr
|
|
96
|
-
|
|
93
|
+
</tr>`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const activeSessions = (sessions || []).filter(s => !s.isUntracked);
|
|
97
|
+
const untrackedSessions = (sessions || []).filter(s => s.isUntracked);
|
|
98
|
+
|
|
99
|
+
const activeRows = activeSessions
|
|
100
|
+
.sort((a, b) => b.cost - a.cost)
|
|
101
|
+
.slice(0, 20)
|
|
102
|
+
.map(makeSessionRow).join('');
|
|
103
|
+
|
|
104
|
+
const untrackedRows = untrackedSessions
|
|
105
|
+
.sort((a, b) => b.cost - a.cost)
|
|
106
|
+
.slice(0, 10)
|
|
107
|
+
.map(makeSessionRow).join('');
|
|
108
|
+
|
|
109
|
+
const untrackedTotal = untrackedSessions.reduce((sum, s) => sum + s.cost, 0);
|
|
110
|
+
const untrackedHidden = Math.max(0, untrackedSessions.length - 10);
|
|
97
111
|
|
|
98
112
|
const html = `<!DOCTYPE html>
|
|
99
113
|
<html lang="en">
|
|
@@ -144,9 +158,14 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
144
158
|
<div class="section" style="border:1px solid #f59e0b;">
|
|
145
159
|
<div class="section-title" style="color:#f59e0b;">💰 Actual API Spend (from transcripts)</div>
|
|
146
160
|
<div style="display:flex; gap:32px; flex-wrap:wrap; align-items:center;">
|
|
161
|
+
${summary.todayCost > 0 ? `
|
|
162
|
+
<div>
|
|
163
|
+
<div style="font-size:36px; font-weight:900; color:#ef4444;">$${summary.todayCost.toFixed(2)}</div>
|
|
164
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Today's spend</div>
|
|
165
|
+
</div>` : ''}
|
|
147
166
|
<div>
|
|
148
|
-
<div style="font-size:36px; font-weight:900; color:#fbbf24;">$${summary.totalRealCost.toFixed(
|
|
149
|
-
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">
|
|
167
|
+
<div style="font-size:36px; font-weight:900; color:#fbbf24;">$${summary.totalRealCost.toFixed(2)}</div>
|
|
168
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">All-time total (API-reported)</div>
|
|
150
169
|
</div>
|
|
151
170
|
${summary.totalEstimatedCost > 0 && summary.totalRealCost > summary.totalEstimatedCost * 1.1 ? `
|
|
152
171
|
<div style="background:#451a03; padding:10px 16px; border-radius:8px; border:1px solid #92400e;">
|
|
@@ -199,17 +218,32 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
199
218
|
</div>
|
|
200
219
|
</div>
|
|
201
220
|
|
|
202
|
-
${
|
|
221
|
+
${activeRows ? `
|
|
203
222
|
<div class="section">
|
|
204
|
-
<div class="section-title">
|
|
223
|
+
<div class="section-title">Active Sessions</div>
|
|
205
224
|
<div style="overflow-x:auto">
|
|
206
225
|
<table>
|
|
207
226
|
<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>
|
|
208
|
-
<tbody>${
|
|
227
|
+
<tbody>${activeRows}</tbody>
|
|
209
228
|
</table>
|
|
210
229
|
</div>
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
<div style="margin-top:8px; font-size:12px; color:#64748b;">● = cost from transcript · ○ = estimated · ⚠️ = orphaned</div>
|
|
231
|
+
</div>` : ''}
|
|
232
|
+
|
|
233
|
+
${untrackedRows ? `
|
|
234
|
+
<div class="section" style="border:1px solid #475569;">
|
|
235
|
+
<div class="section-title" style="color:#94a3b8;">📦 Historical Sessions — $${untrackedTotal.toFixed(2)} total</div>
|
|
236
|
+
<div style="font-size:13px; color:#64748b; margin-bottom:12px;">Old/deleted sessions still on disk. Top 10 by cost.</div>
|
|
237
|
+
<div style="overflow-x:auto">
|
|
238
|
+
<table>
|
|
239
|
+
<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>
|
|
240
|
+
<tbody>${untrackedRows}</tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</div>
|
|
243
|
+
${untrackedHidden > 0 ? `<div style="margin-top:8px; font-size:12px; color:#64748b;">+ ${untrackedHidden} more not shown</div>` : ''}
|
|
244
|
+
</div>` : ''}
|
|
245
|
+
|
|
246
|
+
${burnSummary} : ''}
|
|
213
247
|
|
|
214
248
|
</div>
|
|
215
249
|
<div class="footer">
|
|
@@ -87,30 +87,51 @@ function generateTerminalReport(analysis) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Session breakdown
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Session breakdown — split active from historical
|
|
91
|
+
const activeSess = (sessions || []).filter(s => !s.isUntracked);
|
|
92
|
+
const untrackedSess = (sessions || []).filter(s => s.isUntracked);
|
|
93
|
+
|
|
94
|
+
if (activeSess.length > 0) {
|
|
95
|
+
console.log(`${C}━━━ Active Sessions ━━━${R}\n`);
|
|
96
|
+
const sorted = [...activeSess].sort((a, b) => b.cost - a.cost).slice(0, 8);
|
|
94
97
|
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}${'
|
|
98
|
+
console.log(` ${D}${'\u2500'.repeat(95)}${R}`);
|
|
96
99
|
for (const s of sorted) {
|
|
97
100
|
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
98
|
-
const flag = s.isOrphaned ? '
|
|
99
|
-
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '
|
|
101
|
+
const flag = s.isOrphaned ? ' \u26a0\ufe0f' : '';
|
|
102
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '\u2026' : s.key;
|
|
100
103
|
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(
|
|
104
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : '\u2014';
|
|
105
|
+
console.log(` ${(keyDisplay + flag).padEnd(16)} ${(s.modelLabel || s.model || 'unknown').slice(0, 20).padEnd(20)} ${tok.padEnd(10)} $${s.cost.toFixed(4).padEnd(11)} ${daily.padEnd(10)} ${D}${age}${R}`);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
// Daily burn rate
|
|
106
|
-
const totalDailyRate =
|
|
108
|
+
// Daily burn rate — tracked sessions only
|
|
109
|
+
const totalDailyRate = activeSess.filter(s => s.dailyCost).reduce((sum, s) => sum + s.dailyCost, 0);
|
|
107
110
|
if (totalDailyRate > 0) {
|
|
108
111
|
console.log();
|
|
109
|
-
console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D}
|
|
112
|
+
console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D} \u00b7 ~$${(totalDailyRate * 30).toFixed(2)}/month${R}`);
|
|
113
|
+
}
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (untrackedSess.length > 0) {
|
|
118
|
+
const untrackedTotal = untrackedSess.reduce((sum, s) => sum + s.cost, 0);
|
|
119
|
+
console.log(`${C}━━━ Historical Sessions (${untrackedSess.length} untracked) \u2014 $${untrackedTotal.toFixed(2)} total ━━━${R}\n`);
|
|
120
|
+
const sorted = [...untrackedSess].sort((a, b) => b.cost - a.cost).slice(0, 5);
|
|
121
|
+
console.log(` ${D}${'Session'.padEnd(16)} ${'Model'.padEnd(20)} ${'Tokens'.padEnd(10)} ${'Total Cost'.padEnd(12)} Last Active${R}`);
|
|
122
|
+
console.log(` ${D}${'\u2500'.repeat(80)}${R}`);
|
|
123
|
+
for (const s of sorted) {
|
|
124
|
+
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
125
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '\u2026' : s.key;
|
|
126
|
+
const age = s.ageMs ? `${relativeAge(s.ageMs)} (${new Date(s.updatedAt).toLocaleDateString()})` : 'unknown';
|
|
127
|
+
console.log(` ${keyDisplay.padEnd(16)} ${(s.modelLabel || s.model || 'unknown').slice(0, 20).padEnd(20)} ${tok.padEnd(10)} $${s.cost.toFixed(4).padEnd(11)} ${D}${age}${R}`);
|
|
110
128
|
}
|
|
129
|
+
const hidden = Math.max(0, untrackedSess.length - 5);
|
|
130
|
+
if (hidden > 0) console.log(` ${D}+ ${hidden} more not shown${R}`);
|
|
111
131
|
console.log();
|
|
112
132
|
}
|
|
113
133
|
|
|
134
|
+
|
|
114
135
|
// Summary
|
|
115
136
|
console.log(`${C}━━━ Summary ━━━${R}`);
|
|
116
137
|
console.log(` 🔴 ${RED}${summary.critical}${R} critical 🟠 ${summary.high} high 🟡 ${summary.medium} medium 🔵 ${summary.low||0} low ✅ ${summary.info} ok`);
|
|
@@ -119,7 +140,11 @@ function generateTerminalReport(analysis) {
|
|
|
119
140
|
console.log(` ${D}Cache tokens: ${(summary.totalCacheRead||0).toLocaleString()} read · ${(summary.totalCacheWrite||0).toLocaleString()} write${R}`);
|
|
120
141
|
}
|
|
121
142
|
if (summary.totalRealCost > 0) {
|
|
122
|
-
|
|
143
|
+
if (summary.todayCost > 0) {
|
|
144
|
+
console.log(` ${B}Today's spend: ${RED}$${summary.todayCost.toFixed(2)}${R}${B} · All-time: ${RED}$${summary.totalRealCost.toFixed(2)}${R}${B} (from .jsonl transcripts)${R}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log(` ${B}Actual API spend: ${RED}$${summary.totalRealCost.toFixed(2)}${R}${B} (from .jsonl transcripts)${R}`);
|
|
147
|
+
}
|
|
123
148
|
if (summary.totalEstimatedCost > 0 && summary.totalRealCost > summary.totalEstimatedCost * 1.1) {
|
|
124
149
|
console.log(` ${D}sessions.json estimate: $${summary.totalEstimatedCost.toFixed(4)} — ${(summary.totalRealCost / summary.totalEstimatedCost).toFixed(1)}x gap (cache tokens)${R}`);
|
|
125
150
|
}
|
package/src/analyzer.js
CHANGED
|
@@ -528,13 +528,16 @@ function parseTranscript(jsonlPath) {
|
|
|
528
528
|
|
|
529
529
|
// Only assistant messages with usage blocks have cost data
|
|
530
530
|
if (entry.type !== 'message') continue;
|
|
531
|
-
|
|
531
|
+
|
|
532
|
+
// Usage can be at entry.usage (some formats) or entry.message.usage (standard format)
|
|
533
|
+
const u = entry.usage || entry.message?.usage;
|
|
534
|
+
if (!u) continue;
|
|
532
535
|
|
|
533
536
|
messageCount++;
|
|
534
|
-
const u = entry.usage;
|
|
535
537
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
+
// Model can be at entry.model or entry.message.model
|
|
539
|
+
const entryModel = entry.model || entry.message?.model;
|
|
540
|
+
if (entryModel && !model) model = entryModel;
|
|
538
541
|
|
|
539
542
|
input += u.input || 0;
|
|
540
543
|
output += u.output || 0;
|
|
@@ -706,13 +709,19 @@ function analyzeSessions(sessionsPath) {
|
|
|
706
709
|
// Scan for untracked .jsonl files (sessions not in sessions.json)
|
|
707
710
|
const trackedIds = new Set(Object.values(sessions).map(s => s.sessionId).filter(Boolean));
|
|
708
711
|
let untrackedCount = 0, untrackedCost = 0, untrackedTokens = 0;
|
|
712
|
+
let untrackedRecentCount = 0, untrackedRecentCost = 0;
|
|
713
|
+
const NOW = Date.now();
|
|
714
|
+
const TODAY_START = new Date(); TODAY_START.setHours(0, 0, 0, 0);
|
|
715
|
+
const todayMs = TODAY_START.getTime();
|
|
716
|
+
|
|
709
717
|
if (sessionsDir) {
|
|
710
718
|
try {
|
|
711
719
|
for (const file of fs.readdirSync(sessionsDir)) {
|
|
712
720
|
if (!file.endsWith('.jsonl')) continue;
|
|
713
721
|
const fileId = file.replace('.jsonl', '');
|
|
714
722
|
if (trackedIds.has(fileId)) continue; // already counted
|
|
715
|
-
const
|
|
723
|
+
const filePath = path.join(sessionsDir, file);
|
|
724
|
+
const transcript = parseTranscript(filePath);
|
|
716
725
|
if (transcript && transcript.messageCount > 0) {
|
|
717
726
|
untrackedCount++;
|
|
718
727
|
untrackedCost += transcript.totalCost;
|
|
@@ -720,6 +729,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
720
729
|
totalRealCost += transcript.totalCost;
|
|
721
730
|
totalCacheRead += transcript.cacheRead;
|
|
722
731
|
totalCacheWrite += transcript.cacheWrite;
|
|
732
|
+
|
|
733
|
+
// Check if this file was modified today (recent vs historical)
|
|
734
|
+
let fileMtime = null;
|
|
735
|
+
try { fileMtime = fs.statSync(filePath).mtimeMs; } catch {}
|
|
736
|
+
const isRecent = fileMtime && (NOW - fileMtime < 48 * 3600 * 1000);
|
|
737
|
+
const isToday = fileMtime && fileMtime >= todayMs;
|
|
738
|
+
if (isRecent) { untrackedRecentCount++; untrackedRecentCost += transcript.totalCost; }
|
|
739
|
+
|
|
723
740
|
breakdown.push({
|
|
724
741
|
key: `untracked:${fileId.slice(0, 8)}`, sessionId: fileId, model: transcript.model,
|
|
725
742
|
modelLabel: transcript.model ? (MODEL_PRICING[resolveModel(transcript.model)]?.label || transcript.model) : 'unknown',
|
|
@@ -729,8 +746,9 @@ function analyzeSessions(sessionsPath) {
|
|
|
729
746
|
hasTranscript: true, isSharedSession: false,
|
|
730
747
|
messageCount: transcript.messageCount,
|
|
731
748
|
updatedAt: transcript.lastTs ? new Date(transcript.lastTs).toISOString() : null,
|
|
732
|
-
ageMs: transcript.lastTs ?
|
|
749
|
+
ageMs: transcript.lastTs ? NOW - transcript.lastTs : null,
|
|
733
750
|
dailyCost: null, isOrphaned: false, isUntracked: true,
|
|
751
|
+
isRecent, isToday,
|
|
734
752
|
});
|
|
735
753
|
}
|
|
736
754
|
}
|
|
@@ -738,11 +756,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
738
756
|
}
|
|
739
757
|
|
|
740
758
|
if (untrackedCount > 0) {
|
|
759
|
+
const detail = [`${untrackedTokens.toLocaleString()} tokens · $${untrackedCost.toFixed(4)} total API costs`];
|
|
760
|
+
if (untrackedRecentCount > 0) detail.push(`${untrackedRecentCount} active in last 48h ($${untrackedRecentCost.toFixed(4)})`);
|
|
761
|
+
detail.push('These are sessions not listed in sessions.json — old/deleted or spawned by subprocesses');
|
|
741
762
|
findings.push({
|
|
742
|
-
severity:
|
|
763
|
+
severity: untrackedRecentCost > 1 ? 'high' : (untrackedCost > 1 ? 'medium' : 'info'),
|
|
743
764
|
source: 'sessions',
|
|
744
|
-
message: `${untrackedCount} untracked session(s) found
|
|
745
|
-
detail:
|
|
765
|
+
message: `${untrackedCount} untracked session(s) found (${untrackedRecentCount} recent)`,
|
|
766
|
+
detail: detail.join('\n'),
|
|
746
767
|
});
|
|
747
768
|
}
|
|
748
769
|
|
|
@@ -791,10 +812,22 @@ function analyzeSessions(sessionsPath) {
|
|
|
791
812
|
}
|
|
792
813
|
}
|
|
793
814
|
|
|
815
|
+
// Compute today's cost across all sessions (tracked + untracked)
|
|
816
|
+
let todayCost = 0;
|
|
817
|
+
for (const s of breakdown) {
|
|
818
|
+
if (s.isToday) { todayCost += s.cost; continue; }
|
|
819
|
+
// For tracked sessions, check if updatedAt is today
|
|
820
|
+
if (!s.isUntracked && s.updatedAt) {
|
|
821
|
+
const upd = new Date(s.updatedAt).getTime();
|
|
822
|
+
if (upd >= todayMs) todayCost += s.cost;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
794
826
|
return {
|
|
795
827
|
exists: true, findings, sessions: breakdown,
|
|
796
828
|
totalInputTokens: totalIn, totalOutputTokens: totalOut,
|
|
797
829
|
totalCost, totalCacheRead, totalCacheWrite, totalRealCost,
|
|
830
|
+
todayCost,
|
|
798
831
|
sessionCount: Object.keys(sessions).length,
|
|
799
832
|
};
|
|
800
833
|
}
|
|
@@ -902,6 +935,7 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
|
902
935
|
totalCacheWrite: sessionResult.totalCacheWrite || 0,
|
|
903
936
|
totalRealCost: realCost,
|
|
904
937
|
totalEstimatedCost: sessionResult.totalCost || 0,
|
|
938
|
+
todayCost: sessionResult.todayCost || 0,
|
|
905
939
|
},
|
|
906
940
|
sessions: sessionResult.sessions || [],
|
|
907
941
|
webChatSessions,
|
package/src/htmlReport.js
CHANGED
|
@@ -40,9 +40,9 @@ 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
|
|
45
|
-
const totalDaily =
|
|
43
|
+
// Burn summary calculation — only tracked sessions (not historical untracked)
|
|
44
|
+
const trackedSessions = (sessions || []).filter(s => !s.isUntracked && s.dailyCost);
|
|
45
|
+
const totalDaily = trackedSessions.reduce((sum, s) => sum + (s.dailyCost || 0), 0);
|
|
46
46
|
const totalMonthly = totalDaily * 30;
|
|
47
47
|
const burnSummary = totalDaily > 0 ? `
|
|
48
48
|
<div class="section">
|
|
@@ -50,7 +50,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
50
50
|
<div style="display:flex; gap:32px; flex-wrap:wrap;">
|
|
51
51
|
<div>
|
|
52
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 ${
|
|
53
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Combined daily burn rate across ${trackedSessions.length} active session${trackedSessions.length !== 1 ? 's' : ''}</div>
|
|
54
54
|
</div>
|
|
55
55
|
<div>
|
|
56
56
|
<div style="font-size:24px; font-weight:800; color:#f97316;">$${totalMonthly.toFixed(2)}/month</div>
|
|
@@ -73,10 +73,7 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
73
73
|
</div>
|
|
74
74
|
`).join('');
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
78
|
-
.slice(0, 20)
|
|
79
|
-
.map(s => {
|
|
76
|
+
const makeSessionRow = (s) => {
|
|
80
77
|
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '…' : s.key;
|
|
81
78
|
const flag = s.isOrphaned ? ' ⚠️' : '';
|
|
82
79
|
const txBadge = s.hasTranscript ? '<span style="color:#22c55e; font-size:10px; margin-left:4px" title="Cost from .jsonl transcript (API-reported)">●</span>' : '<span style="color:#6b7280; font-size:10px; margin-left:4px" title="Estimated cost (no transcript found)">○</span>';
|
|
@@ -84,16 +81,33 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
84
81
|
const absDate = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '';
|
|
85
82
|
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}/day` : '—';
|
|
86
83
|
const cacheTitle = (s.cacheRead || s.cacheWrite) ? `Cache R: ${(s.cacheRead||0).toLocaleString()} · W: ${(s.cacheWrite||0).toLocaleString()}` : '';
|
|
84
|
+
const rowBg = s.isOrphaned ? 'background:#fff7ed' : (s.isRecent ? 'background:#fffbeb' : '');
|
|
87
85
|
return `
|
|
88
|
-
<tr style="${
|
|
86
|
+
<tr style="${rowBg}">
|
|
89
87
|
<td style="padding:8px 12px; font-family:monospace; font-size:13px">${keyDisplay}${flag}${txBadge}</td>
|
|
90
88
|
<td style="padding:8px 12px">${s.modelLabel || s.model}</td>
|
|
91
89
|
<td style="padding:8px 12px; text-align:right" title="${cacheTitle}">${(s.inputTokens + s.outputTokens).toLocaleString()}${(s.cacheRead || s.cacheWrite) ? `<span style="color:#6b7280; font-size:11px;"> +${((s.cacheRead||0)+(s.cacheWrite||0)).toLocaleString()} cache</span>` : ''}</td>
|
|
92
|
-
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(
|
|
90
|
+
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(4)}</td>
|
|
93
91
|
<td style="padding:8px 12px; text-align:right; color:#f59e0b">${daily}</td>
|
|
94
92
|
<td style="padding:8px 12px; color:#6b7280; font-size:13px" title="${absDate}">${age}</td>
|
|
95
|
-
</tr
|
|
96
|
-
|
|
93
|
+
</tr>`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const activeSessions = (sessions || []).filter(s => !s.isUntracked);
|
|
97
|
+
const untrackedSessions = (sessions || []).filter(s => s.isUntracked);
|
|
98
|
+
|
|
99
|
+
const activeRows = activeSessions
|
|
100
|
+
.sort((a, b) => b.cost - a.cost)
|
|
101
|
+
.slice(0, 20)
|
|
102
|
+
.map(makeSessionRow).join('');
|
|
103
|
+
|
|
104
|
+
const untrackedRows = untrackedSessions
|
|
105
|
+
.sort((a, b) => b.cost - a.cost)
|
|
106
|
+
.slice(0, 10)
|
|
107
|
+
.map(makeSessionRow).join('');
|
|
108
|
+
|
|
109
|
+
const untrackedTotal = untrackedSessions.reduce((sum, s) => sum + s.cost, 0);
|
|
110
|
+
const untrackedHidden = Math.max(0, untrackedSessions.length - 10);
|
|
97
111
|
|
|
98
112
|
const html = `<!DOCTYPE html>
|
|
99
113
|
<html lang="en">
|
|
@@ -144,9 +158,14 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
144
158
|
<div class="section" style="border:1px solid #f59e0b;">
|
|
145
159
|
<div class="section-title" style="color:#f59e0b;">💰 Actual API Spend (from transcripts)</div>
|
|
146
160
|
<div style="display:flex; gap:32px; flex-wrap:wrap; align-items:center;">
|
|
161
|
+
${summary.todayCost > 0 ? `
|
|
162
|
+
<div>
|
|
163
|
+
<div style="font-size:36px; font-weight:900; color:#ef4444;">$${summary.todayCost.toFixed(2)}</div>
|
|
164
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">Today's spend</div>
|
|
165
|
+
</div>` : ''}
|
|
147
166
|
<div>
|
|
148
|
-
<div style="font-size:36px; font-weight:900; color:#fbbf24;">$${summary.totalRealCost.toFixed(
|
|
149
|
-
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">
|
|
167
|
+
<div style="font-size:36px; font-weight:900; color:#fbbf24;">$${summary.totalRealCost.toFixed(2)}</div>
|
|
168
|
+
<div style="font-size:13px; color:#94a3b8; margin-top:4px;">All-time total (API-reported)</div>
|
|
150
169
|
</div>
|
|
151
170
|
${summary.totalEstimatedCost > 0 && summary.totalRealCost > summary.totalEstimatedCost * 1.1 ? `
|
|
152
171
|
<div style="background:#451a03; padding:10px 16px; border-radius:8px; border:1px solid #92400e;">
|
|
@@ -199,17 +218,32 @@ async function generateHTMLReport(analysis, outPath) {
|
|
|
199
218
|
</div>
|
|
200
219
|
</div>
|
|
201
220
|
|
|
202
|
-
${
|
|
221
|
+
${activeRows ? `
|
|
203
222
|
<div class="section">
|
|
204
|
-
<div class="section-title">
|
|
223
|
+
<div class="section-title">Active Sessions</div>
|
|
205
224
|
<div style="overflow-x:auto">
|
|
206
225
|
<table>
|
|
207
226
|
<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>
|
|
208
|
-
<tbody>${
|
|
227
|
+
<tbody>${activeRows}</tbody>
|
|
209
228
|
</table>
|
|
210
229
|
</div>
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
<div style="margin-top:8px; font-size:12px; color:#64748b;">● = cost from transcript · ○ = estimated · ⚠️ = orphaned</div>
|
|
231
|
+
</div>` : ''}
|
|
232
|
+
|
|
233
|
+
${untrackedRows ? `
|
|
234
|
+
<div class="section" style="border:1px solid #475569;">
|
|
235
|
+
<div class="section-title" style="color:#94a3b8;">📦 Historical Sessions — $${untrackedTotal.toFixed(2)} total</div>
|
|
236
|
+
<div style="font-size:13px; color:#64748b; margin-bottom:12px;">Old/deleted sessions still on disk. Top 10 by cost.</div>
|
|
237
|
+
<div style="overflow-x:auto">
|
|
238
|
+
<table>
|
|
239
|
+
<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>
|
|
240
|
+
<tbody>${untrackedRows}</tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</div>
|
|
243
|
+
${untrackedHidden > 0 ? `<div style="margin-top:8px; font-size:12px; color:#64748b;">+ ${untrackedHidden} more not shown</div>` : ''}
|
|
244
|
+
</div>` : ''}
|
|
245
|
+
|
|
246
|
+
${burnSummary} : ''}
|
|
213
247
|
|
|
214
248
|
</div>
|
|
215
249
|
<div class="footer">
|
package/src/reporter.js
CHANGED
|
@@ -87,30 +87,51 @@ function generateTerminalReport(analysis) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Session breakdown
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Session breakdown — split active from historical
|
|
91
|
+
const activeSess = (sessions || []).filter(s => !s.isUntracked);
|
|
92
|
+
const untrackedSess = (sessions || []).filter(s => s.isUntracked);
|
|
93
|
+
|
|
94
|
+
if (activeSess.length > 0) {
|
|
95
|
+
console.log(`${C}━━━ Active Sessions ━━━${R}\n`);
|
|
96
|
+
const sorted = [...activeSess].sort((a, b) => b.cost - a.cost).slice(0, 8);
|
|
94
97
|
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}${'
|
|
98
|
+
console.log(` ${D}${'\u2500'.repeat(95)}${R}`);
|
|
96
99
|
for (const s of sorted) {
|
|
97
100
|
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
98
|
-
const flag = s.isOrphaned ? '
|
|
99
|
-
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '
|
|
101
|
+
const flag = s.isOrphaned ? ' \u26a0\ufe0f' : '';
|
|
102
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '\u2026' : s.key;
|
|
100
103
|
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(
|
|
104
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : '\u2014';
|
|
105
|
+
console.log(` ${(keyDisplay + flag).padEnd(16)} ${(s.modelLabel || s.model || 'unknown').slice(0, 20).padEnd(20)} ${tok.padEnd(10)} $${s.cost.toFixed(4).padEnd(11)} ${daily.padEnd(10)} ${D}${age}${R}`);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
// Daily burn rate
|
|
106
|
-
const totalDailyRate =
|
|
108
|
+
// Daily burn rate — tracked sessions only
|
|
109
|
+
const totalDailyRate = activeSess.filter(s => s.dailyCost).reduce((sum, s) => sum + s.dailyCost, 0);
|
|
107
110
|
if (totalDailyRate > 0) {
|
|
108
111
|
console.log();
|
|
109
|
-
console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D}
|
|
112
|
+
console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D} \u00b7 ~$${(totalDailyRate * 30).toFixed(2)}/month${R}`);
|
|
113
|
+
}
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (untrackedSess.length > 0) {
|
|
118
|
+
const untrackedTotal = untrackedSess.reduce((sum, s) => sum + s.cost, 0);
|
|
119
|
+
console.log(`${C}━━━ Historical Sessions (${untrackedSess.length} untracked) \u2014 $${untrackedTotal.toFixed(2)} total ━━━${R}\n`);
|
|
120
|
+
const sorted = [...untrackedSess].sort((a, b) => b.cost - a.cost).slice(0, 5);
|
|
121
|
+
console.log(` ${D}${'Session'.padEnd(16)} ${'Model'.padEnd(20)} ${'Tokens'.padEnd(10)} ${'Total Cost'.padEnd(12)} Last Active${R}`);
|
|
122
|
+
console.log(` ${D}${'\u2500'.repeat(80)}${R}`);
|
|
123
|
+
for (const s of sorted) {
|
|
124
|
+
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
125
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + '\u2026' : s.key;
|
|
126
|
+
const age = s.ageMs ? `${relativeAge(s.ageMs)} (${new Date(s.updatedAt).toLocaleDateString()})` : 'unknown';
|
|
127
|
+
console.log(` ${keyDisplay.padEnd(16)} ${(s.modelLabel || s.model || 'unknown').slice(0, 20).padEnd(20)} ${tok.padEnd(10)} $${s.cost.toFixed(4).padEnd(11)} ${D}${age}${R}`);
|
|
110
128
|
}
|
|
129
|
+
const hidden = Math.max(0, untrackedSess.length - 5);
|
|
130
|
+
if (hidden > 0) console.log(` ${D}+ ${hidden} more not shown${R}`);
|
|
111
131
|
console.log();
|
|
112
132
|
}
|
|
113
133
|
|
|
134
|
+
|
|
114
135
|
// Summary
|
|
115
136
|
console.log(`${C}━━━ Summary ━━━${R}`);
|
|
116
137
|
console.log(` 🔴 ${RED}${summary.critical}${R} critical 🟠 ${summary.high} high 🟡 ${summary.medium} medium 🔵 ${summary.low||0} low ✅ ${summary.info} ok`);
|
|
@@ -119,7 +140,11 @@ function generateTerminalReport(analysis) {
|
|
|
119
140
|
console.log(` ${D}Cache tokens: ${(summary.totalCacheRead||0).toLocaleString()} read · ${(summary.totalCacheWrite||0).toLocaleString()} write${R}`);
|
|
120
141
|
}
|
|
121
142
|
if (summary.totalRealCost > 0) {
|
|
122
|
-
|
|
143
|
+
if (summary.todayCost > 0) {
|
|
144
|
+
console.log(` ${B}Today's spend: ${RED}$${summary.todayCost.toFixed(2)}${R}${B} · All-time: ${RED}$${summary.totalRealCost.toFixed(2)}${R}${B} (from .jsonl transcripts)${R}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log(` ${B}Actual API spend: ${RED}$${summary.totalRealCost.toFixed(2)}${R}${B} (from .jsonl transcripts)${R}`);
|
|
147
|
+
}
|
|
123
148
|
if (summary.totalEstimatedCost > 0 && summary.totalRealCost > summary.totalEstimatedCost * 1.1) {
|
|
124
149
|
console.log(` ${D}sessions.json estimate: $${summary.totalEstimatedCost.toFixed(4)} — ${(summary.totalRealCost / summary.totalEstimatedCost).toFixed(1)}x gap (cache tokens)${R}`);
|
|
125
150
|
}
|