clawculator 2.2.2 → 2.3.1
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 +37 -6
- package/skills/clawculator/htmlReport.js +53 -19
- package/skills/clawculator/reporter.js +38 -13
- package/src/analyzer.js +37 -6
- package/src/htmlReport.js +53 -19
- package/src/reporter.js +38 -13
package/package.json
CHANGED
|
@@ -690,7 +690,7 @@ function analyzeSessions(sessionsPath) {
|
|
|
690
690
|
|
|
691
691
|
const ageMs = updatedAt ? Date.now() - new Date(updatedAt).getTime() : null;
|
|
692
692
|
const ageDays = ageMs ? ageMs / (1000 * 3600 * 24) : null;
|
|
693
|
-
const dailyCost = (ageDays && ageDays > 0
|
|
693
|
+
const dailyCost = (ageDays && ageDays > 1.0 && realCost > 0) ? realCost / ageDays : null;
|
|
694
694
|
|
|
695
695
|
const realModelKey = resolveModel(realModel);
|
|
696
696
|
breakdown.push({
|
|
@@ -709,13 +709,19 @@ function analyzeSessions(sessionsPath) {
|
|
|
709
709
|
// Scan for untracked .jsonl files (sessions not in sessions.json)
|
|
710
710
|
const trackedIds = new Set(Object.values(sessions).map(s => s.sessionId).filter(Boolean));
|
|
711
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
|
+
|
|
712
717
|
if (sessionsDir) {
|
|
713
718
|
try {
|
|
714
719
|
for (const file of fs.readdirSync(sessionsDir)) {
|
|
715
720
|
if (!file.endsWith('.jsonl')) continue;
|
|
716
721
|
const fileId = file.replace('.jsonl', '');
|
|
717
722
|
if (trackedIds.has(fileId)) continue; // already counted
|
|
718
|
-
const
|
|
723
|
+
const filePath = path.join(sessionsDir, file);
|
|
724
|
+
const transcript = parseTranscript(filePath);
|
|
719
725
|
if (transcript && transcript.messageCount > 0) {
|
|
720
726
|
untrackedCount++;
|
|
721
727
|
untrackedCost += transcript.totalCost;
|
|
@@ -723,6 +729,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
723
729
|
totalRealCost += transcript.totalCost;
|
|
724
730
|
totalCacheRead += transcript.cacheRead;
|
|
725
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
|
+
|
|
726
740
|
breakdown.push({
|
|
727
741
|
key: `untracked:${fileId.slice(0, 8)}`, sessionId: fileId, model: transcript.model,
|
|
728
742
|
modelLabel: transcript.model ? (MODEL_PRICING[resolveModel(transcript.model)]?.label || transcript.model) : 'unknown',
|
|
@@ -732,8 +746,9 @@ function analyzeSessions(sessionsPath) {
|
|
|
732
746
|
hasTranscript: true, isSharedSession: false,
|
|
733
747
|
messageCount: transcript.messageCount,
|
|
734
748
|
updatedAt: transcript.lastTs ? new Date(transcript.lastTs).toISOString() : null,
|
|
735
|
-
ageMs: transcript.lastTs ?
|
|
749
|
+
ageMs: transcript.lastTs ? NOW - transcript.lastTs : null,
|
|
736
750
|
dailyCost: null, isOrphaned: false, isUntracked: true,
|
|
751
|
+
isRecent, isToday,
|
|
737
752
|
});
|
|
738
753
|
}
|
|
739
754
|
}
|
|
@@ -741,11 +756,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
741
756
|
}
|
|
742
757
|
|
|
743
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');
|
|
744
762
|
findings.push({
|
|
745
|
-
severity:
|
|
763
|
+
severity: untrackedRecentCost > 1 ? 'high' : (untrackedCost > 1 ? 'medium' : 'info'),
|
|
746
764
|
source: 'sessions',
|
|
747
|
-
message: `${untrackedCount} untracked session(s) found
|
|
748
|
-
detail:
|
|
765
|
+
message: `${untrackedCount} untracked session(s) found (${untrackedRecentCount} recent)`,
|
|
766
|
+
detail: detail.join('\n'),
|
|
749
767
|
});
|
|
750
768
|
}
|
|
751
769
|
|
|
@@ -794,10 +812,22 @@ function analyzeSessions(sessionsPath) {
|
|
|
794
812
|
}
|
|
795
813
|
}
|
|
796
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
|
+
|
|
797
826
|
return {
|
|
798
827
|
exists: true, findings, sessions: breakdown,
|
|
799
828
|
totalInputTokens: totalIn, totalOutputTokens: totalOut,
|
|
800
829
|
totalCost, totalCacheRead, totalCacheWrite, totalRealCost,
|
|
830
|
+
todayCost,
|
|
801
831
|
sessionCount: Object.keys(sessions).length,
|
|
802
832
|
};
|
|
803
833
|
}
|
|
@@ -905,6 +935,7 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
|
905
935
|
totalCacheWrite: sessionResult.totalCacheWrite || 0,
|
|
906
936
|
totalRealCost: realCost,
|
|
907
937
|
totalEstimatedCost: sessionResult.totalCost || 0,
|
|
938
|
+
todayCost: sessionResult.todayCost || 0,
|
|
908
939
|
},
|
|
909
940
|
sessions: sessionResult.sessions || [],
|
|
910
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
|
@@ -690,7 +690,7 @@ function analyzeSessions(sessionsPath) {
|
|
|
690
690
|
|
|
691
691
|
const ageMs = updatedAt ? Date.now() - new Date(updatedAt).getTime() : null;
|
|
692
692
|
const ageDays = ageMs ? ageMs / (1000 * 3600 * 24) : null;
|
|
693
|
-
const dailyCost = (ageDays && ageDays > 0
|
|
693
|
+
const dailyCost = (ageDays && ageDays > 1.0 && realCost > 0) ? realCost / ageDays : null;
|
|
694
694
|
|
|
695
695
|
const realModelKey = resolveModel(realModel);
|
|
696
696
|
breakdown.push({
|
|
@@ -709,13 +709,19 @@ function analyzeSessions(sessionsPath) {
|
|
|
709
709
|
// Scan for untracked .jsonl files (sessions not in sessions.json)
|
|
710
710
|
const trackedIds = new Set(Object.values(sessions).map(s => s.sessionId).filter(Boolean));
|
|
711
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
|
+
|
|
712
717
|
if (sessionsDir) {
|
|
713
718
|
try {
|
|
714
719
|
for (const file of fs.readdirSync(sessionsDir)) {
|
|
715
720
|
if (!file.endsWith('.jsonl')) continue;
|
|
716
721
|
const fileId = file.replace('.jsonl', '');
|
|
717
722
|
if (trackedIds.has(fileId)) continue; // already counted
|
|
718
|
-
const
|
|
723
|
+
const filePath = path.join(sessionsDir, file);
|
|
724
|
+
const transcript = parseTranscript(filePath);
|
|
719
725
|
if (transcript && transcript.messageCount > 0) {
|
|
720
726
|
untrackedCount++;
|
|
721
727
|
untrackedCost += transcript.totalCost;
|
|
@@ -723,6 +729,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
723
729
|
totalRealCost += transcript.totalCost;
|
|
724
730
|
totalCacheRead += transcript.cacheRead;
|
|
725
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
|
+
|
|
726
740
|
breakdown.push({
|
|
727
741
|
key: `untracked:${fileId.slice(0, 8)}`, sessionId: fileId, model: transcript.model,
|
|
728
742
|
modelLabel: transcript.model ? (MODEL_PRICING[resolveModel(transcript.model)]?.label || transcript.model) : 'unknown',
|
|
@@ -732,8 +746,9 @@ function analyzeSessions(sessionsPath) {
|
|
|
732
746
|
hasTranscript: true, isSharedSession: false,
|
|
733
747
|
messageCount: transcript.messageCount,
|
|
734
748
|
updatedAt: transcript.lastTs ? new Date(transcript.lastTs).toISOString() : null,
|
|
735
|
-
ageMs: transcript.lastTs ?
|
|
749
|
+
ageMs: transcript.lastTs ? NOW - transcript.lastTs : null,
|
|
736
750
|
dailyCost: null, isOrphaned: false, isUntracked: true,
|
|
751
|
+
isRecent, isToday,
|
|
737
752
|
});
|
|
738
753
|
}
|
|
739
754
|
}
|
|
@@ -741,11 +756,14 @@ function analyzeSessions(sessionsPath) {
|
|
|
741
756
|
}
|
|
742
757
|
|
|
743
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');
|
|
744
762
|
findings.push({
|
|
745
|
-
severity:
|
|
763
|
+
severity: untrackedRecentCost > 1 ? 'high' : (untrackedCost > 1 ? 'medium' : 'info'),
|
|
746
764
|
source: 'sessions',
|
|
747
|
-
message: `${untrackedCount} untracked session(s) found
|
|
748
|
-
detail:
|
|
765
|
+
message: `${untrackedCount} untracked session(s) found (${untrackedRecentCount} recent)`,
|
|
766
|
+
detail: detail.join('\n'),
|
|
749
767
|
});
|
|
750
768
|
}
|
|
751
769
|
|
|
@@ -794,10 +812,22 @@ function analyzeSessions(sessionsPath) {
|
|
|
794
812
|
}
|
|
795
813
|
}
|
|
796
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
|
+
|
|
797
826
|
return {
|
|
798
827
|
exists: true, findings, sessions: breakdown,
|
|
799
828
|
totalInputTokens: totalIn, totalOutputTokens: totalOut,
|
|
800
829
|
totalCost, totalCacheRead, totalCacheWrite, totalRealCost,
|
|
830
|
+
todayCost,
|
|
801
831
|
sessionCount: Object.keys(sessions).length,
|
|
802
832
|
};
|
|
803
833
|
}
|
|
@@ -905,6 +935,7 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
|
905
935
|
totalCacheWrite: sessionResult.totalCacheWrite || 0,
|
|
906
936
|
totalRealCost: realCost,
|
|
907
937
|
totalEstimatedCost: sessionResult.totalCost || 0,
|
|
938
|
+
todayCost: sessionResult.todayCost || 0,
|
|
908
939
|
},
|
|
909
940
|
sessions: sessionResult.sessions || [],
|
|
910
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
|
}
|