clawculator 2.6.1 → 2.8.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/README.md +101 -36
- package/bin/clawculator.js +30 -0
- package/package.json +4 -3
- package/skills/clawculator/analyzer.js +43 -11
- package/skills/clawculator/snapshotCard.js +324 -0
- package/skills/clawculator/webDashboard.js +7 -1
- package/src/analyzer.js +43 -11
- package/src/snapshotCard.js +324 -0
- package/src/webDashboard.js +7 -1
- package/clawculator-sync.patch +0 -452
- package/clawculator-v2.2.0.patch +0 -1233
- package/src/clawculator.js +0 -123
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a shareable cost snapshot card.
|
|
8
|
+
* Square format — works on Twitter, Instagram, TikTok, Discord.
|
|
9
|
+
* Shows grade, cost range, setup complexity — no exact dollars or session names.
|
|
10
|
+
*/
|
|
11
|
+
function generateSnapshotCard(analysis, outputDir) {
|
|
12
|
+
const s = analysis.summary;
|
|
13
|
+
|
|
14
|
+
// ── Compute grade ──────────────────────────────────────
|
|
15
|
+
const criticals = s.critical || 0;
|
|
16
|
+
const highs = s.high || 0;
|
|
17
|
+
const mediums = s.medium || 0;
|
|
18
|
+
const totalFindings = criticals + highs + mediums;
|
|
19
|
+
|
|
20
|
+
let grade, gradeColor, gradeGlow, gradeEmoji;
|
|
21
|
+
if (criticals === 0 && highs === 0 && mediums === 0) {
|
|
22
|
+
grade = 'A+'; gradeColor = '#22c55e'; gradeGlow = 'rgba(34,197,94,0.3)'; gradeEmoji = '🏆';
|
|
23
|
+
} else if (criticals === 0 && highs <= 1) {
|
|
24
|
+
grade = 'A'; gradeColor = '#22c55e'; gradeGlow = 'rgba(34,197,94,0.2)'; gradeEmoji = '✅';
|
|
25
|
+
} else if (criticals <= 1 && highs <= 2) {
|
|
26
|
+
grade = 'B'; gradeColor = '#f59e0b'; gradeGlow = 'rgba(245,158,11,0.2)'; gradeEmoji = '👍';
|
|
27
|
+
} else if (criticals <= 2) {
|
|
28
|
+
grade = 'C'; gradeColor = '#f97316'; gradeGlow = 'rgba(249,115,22,0.2)'; gradeEmoji = '⚠️';
|
|
29
|
+
} else {
|
|
30
|
+
grade = 'D'; gradeColor = '#ef4444'; gradeGlow = 'rgba(239,68,68,0.2)'; gradeEmoji = '🔥';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Cost range (bucketed, not exact) ───────────────────
|
|
34
|
+
const todayCost = s.todayCost || 0;
|
|
35
|
+
let costRange, costEmoji;
|
|
36
|
+
if (todayCost === 0) { costRange = 'Free tier'; costEmoji = '🆓'; }
|
|
37
|
+
else if (todayCost < 0.50) { costRange = 'Under $0.50/day'; costEmoji = '💲'; }
|
|
38
|
+
else if (todayCost < 1) { costRange = 'Under $1/day'; costEmoji = '💲'; }
|
|
39
|
+
else if (todayCost < 5) { costRange = '$1–5/day'; costEmoji = '💵'; }
|
|
40
|
+
else if (todayCost < 10) { costRange = '$5–10/day'; costEmoji = '💰'; }
|
|
41
|
+
else if (todayCost < 25) { costRange = '$10–25/day'; costEmoji = '💰'; }
|
|
42
|
+
else if (todayCost < 50) { costRange = '$25–50/day'; costEmoji = '🔥'; }
|
|
43
|
+
else { costRange = '$50+/day'; costEmoji = '🚨'; }
|
|
44
|
+
|
|
45
|
+
// ── Setup complexity from config ───────────────────────
|
|
46
|
+
const config = analysis.config || {};
|
|
47
|
+
|
|
48
|
+
const channels = [];
|
|
49
|
+
if (config.channels?.whatsapp) channels.push('WhatsApp');
|
|
50
|
+
if (config.channels?.telegram) channels.push('Telegram');
|
|
51
|
+
if (config.channels?.discord) channels.push('Discord');
|
|
52
|
+
if (config.channels?.signal) channels.push('Signal');
|
|
53
|
+
if (config.channels?.slack) channels.push('Slack');
|
|
54
|
+
if (config.webchat || config.channels?.webchat) channels.push('Webchat');
|
|
55
|
+
|
|
56
|
+
const skillCount = config.skills ? Object.keys(config.skills).length : 0;
|
|
57
|
+
const cronCount = config.cron ? Object.keys(config.cron).length : 0;
|
|
58
|
+
const hookCount = config.hooks ? Object.keys(config.hooks).length : 0;
|
|
59
|
+
const agentCount = config.agents?.list ? Object.keys(config.agents.list).length : 1;
|
|
60
|
+
const sessionCount = s.sessionsAnalyzed || 0;
|
|
61
|
+
|
|
62
|
+
const modelSet = new Set();
|
|
63
|
+
for (const sess of (analysis.sessions || [])) {
|
|
64
|
+
if (sess.model || sess.modelLabel) modelSet.add(sess.modelLabel || sess.model);
|
|
65
|
+
}
|
|
66
|
+
const modelCount = modelSet.size || 1;
|
|
67
|
+
|
|
68
|
+
const totalTokens = s.totalTokensFound || 1;
|
|
69
|
+
const cacheEfficiency = Math.round((s.totalCacheRead || 0) / totalTokens * 100);
|
|
70
|
+
|
|
71
|
+
// ── Build stat pills ──────────────────────────────────
|
|
72
|
+
const pills = [];
|
|
73
|
+
if (channels.length > 0) pills.push({ icon: '📱', label: `${channels.length} channel${channels.length>1?'s':''}` });
|
|
74
|
+
if (skillCount > 0) pills.push({ icon: '🔧', label: `${skillCount} skill${skillCount>1?'s':''}` });
|
|
75
|
+
if (cronCount > 0) pills.push({ icon: '⏰', label: `${cronCount} cron${cronCount>1?'s':''}` });
|
|
76
|
+
if (hookCount > 0) pills.push({ icon: '🪝', label: `${hookCount} hook${hookCount>1?'s':''}` });
|
|
77
|
+
if (agentCount > 1) pills.push({ icon: '🤖', label: `${agentCount} agents` });
|
|
78
|
+
if (sessionCount > 0) pills.push({ icon: '💬', label: `${sessionCount} sessions` });
|
|
79
|
+
pills.push({ icon: '🧠', label: `${modelCount} model${modelCount>1?'s':''}` });
|
|
80
|
+
if (cacheEfficiency > 0) pills.push({ icon: '⚡', label: `${cacheEfficiency}% cache` });
|
|
81
|
+
|
|
82
|
+
// ── Findings summary ──────────────────────────────────
|
|
83
|
+
const findingSummary = [];
|
|
84
|
+
if (criticals > 0) findingSummary.push(`🔴 ${criticals} critical`);
|
|
85
|
+
if (highs > 0) findingSummary.push(`🟠 ${highs} high`);
|
|
86
|
+
if (mediums > 0) findingSummary.push(`🟡 ${mediums} medium`);
|
|
87
|
+
|
|
88
|
+
const html = `<!DOCTYPE html>
|
|
89
|
+
<html lang="en">
|
|
90
|
+
<head>
|
|
91
|
+
<meta charset="UTF-8">
|
|
92
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
93
|
+
<title>Clawculator Snapshot</title>
|
|
94
|
+
<style>
|
|
95
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&family=Outfit:wght@400;500;600;700;800;900&display=swap');
|
|
96
|
+
|
|
97
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
98
|
+
|
|
99
|
+
html, body {
|
|
100
|
+
min-height: 100vh;
|
|
101
|
+
font-family: 'Outfit', sans-serif;
|
|
102
|
+
background: #020408;
|
|
103
|
+
color: #e2e8f0;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.card {
|
|
110
|
+
width: 480px; max-width: 96vw;
|
|
111
|
+
background: #05080f;
|
|
112
|
+
border-radius: 24px;
|
|
113
|
+
border: 1px solid #1a2744;
|
|
114
|
+
padding: 32px 28px 24px;
|
|
115
|
+
display: flex; flex-direction: column;
|
|
116
|
+
align-items: center;
|
|
117
|
+
position: relative;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
box-shadow: 0 0 80px rgba(34,211,238,0.04), 0 20px 60px rgba(0,0,0,0.5);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Grid bg */
|
|
123
|
+
.card::before {
|
|
124
|
+
content: '';
|
|
125
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
126
|
+
background:
|
|
127
|
+
linear-gradient(rgba(34,211,238,0.02) 1px, transparent 1px),
|
|
128
|
+
linear-gradient(90deg, rgba(34,211,238,0.02) 1px, transparent 1px);
|
|
129
|
+
background-size: 32px 32px;
|
|
130
|
+
pointer-events: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.card > * { position: relative; z-index: 1; }
|
|
134
|
+
|
|
135
|
+
/* Header */
|
|
136
|
+
.header {
|
|
137
|
+
display: flex; align-items: center; gap: 10px;
|
|
138
|
+
margin-bottom: 20px;
|
|
139
|
+
}
|
|
140
|
+
.logo-claw { font-size: 28px; }
|
|
141
|
+
.logo-text {
|
|
142
|
+
font-family: 'JetBrains Mono', monospace;
|
|
143
|
+
font-weight: 800; font-size: 18px; letter-spacing: -0.5px;
|
|
144
|
+
background: linear-gradient(135deg, #22d3ee, #818cf8);
|
|
145
|
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Grade */
|
|
149
|
+
.grade-container {
|
|
150
|
+
display: flex; flex-direction: column; align-items: center;
|
|
151
|
+
margin-bottom: 16px;
|
|
152
|
+
}
|
|
153
|
+
.grade-ring {
|
|
154
|
+
width: 120px; height: 120px;
|
|
155
|
+
border-radius: 50%;
|
|
156
|
+
border: 3px solid ${gradeColor}44;
|
|
157
|
+
display: flex; align-items: center; justify-content: center;
|
|
158
|
+
box-shadow: 0 0 40px ${gradeGlow}, inset 0 0 20px ${gradeGlow};
|
|
159
|
+
position: relative;
|
|
160
|
+
}
|
|
161
|
+
.grade-ring::before {
|
|
162
|
+
content: '';
|
|
163
|
+
position: absolute; inset: -2px;
|
|
164
|
+
border-radius: 50%;
|
|
165
|
+
border: 2px solid ${gradeColor};
|
|
166
|
+
opacity: 0.5;
|
|
167
|
+
}
|
|
168
|
+
.grade-letter {
|
|
169
|
+
font-family: 'JetBrains Mono', monospace;
|
|
170
|
+
font-weight: 900; font-size: 52px;
|
|
171
|
+
color: ${gradeColor};
|
|
172
|
+
text-shadow: 0 0 20px ${gradeGlow};
|
|
173
|
+
line-height: 1;
|
|
174
|
+
}
|
|
175
|
+
.grade-label {
|
|
176
|
+
font-size: 11px; font-weight: 600; color: #64748b;
|
|
177
|
+
text-transform: uppercase; letter-spacing: 1.5px;
|
|
178
|
+
margin-top: 8px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Cost */
|
|
182
|
+
.cost-range {
|
|
183
|
+
font-family: 'JetBrains Mono', monospace;
|
|
184
|
+
font-weight: 700; font-size: 22px;
|
|
185
|
+
color: #e2e8f0;
|
|
186
|
+
margin-bottom: 14px;
|
|
187
|
+
text-align: center;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Divider */
|
|
191
|
+
.divider {
|
|
192
|
+
width: 60px; height: 1px;
|
|
193
|
+
background: linear-gradient(90deg, transparent, #1e3a5f, transparent);
|
|
194
|
+
margin-bottom: 14px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Pills */
|
|
198
|
+
.pills {
|
|
199
|
+
display: flex; flex-wrap: wrap; justify-content: center;
|
|
200
|
+
gap: 6px;
|
|
201
|
+
margin-bottom: 14px;
|
|
202
|
+
max-width: 400px;
|
|
203
|
+
}
|
|
204
|
+
.pill {
|
|
205
|
+
display: flex; align-items: center; gap: 4px;
|
|
206
|
+
background: #0b1120;
|
|
207
|
+
border: 1px solid #1a2744;
|
|
208
|
+
border-radius: 14px;
|
|
209
|
+
padding: 5px 12px;
|
|
210
|
+
font-size: 12px; font-weight: 500;
|
|
211
|
+
color: #94a3b8;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Findings */
|
|
215
|
+
.findings {
|
|
216
|
+
text-align: center;
|
|
217
|
+
margin-bottom: 14px;
|
|
218
|
+
}
|
|
219
|
+
.findings-badges {
|
|
220
|
+
display: flex; justify-content: center; gap: 8px;
|
|
221
|
+
flex-wrap: wrap;
|
|
222
|
+
}
|
|
223
|
+
.findings-badge {
|
|
224
|
+
font-family: 'JetBrains Mono', monospace;
|
|
225
|
+
font-size: 12px; font-weight: 600;
|
|
226
|
+
padding: 4px 10px; border-radius: 6px;
|
|
227
|
+
}
|
|
228
|
+
.findings-clean {
|
|
229
|
+
font-size: 14px; color: #22c55e; font-weight: 600;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* CTA */
|
|
233
|
+
.cta {
|
|
234
|
+
margin-top: auto;
|
|
235
|
+
text-align: center;
|
|
236
|
+
width: 100%;
|
|
237
|
+
}
|
|
238
|
+
.cta-divider {
|
|
239
|
+
width: 100%; height: 1px;
|
|
240
|
+
background: linear-gradient(90deg, transparent, #1a2744, transparent);
|
|
241
|
+
margin-bottom: 14px;
|
|
242
|
+
}
|
|
243
|
+
.cta-prompt {
|
|
244
|
+
font-size: 13px; color: #64748b;
|
|
245
|
+
margin-bottom: 8px;
|
|
246
|
+
}
|
|
247
|
+
.cta-command {
|
|
248
|
+
font-family: 'JetBrains Mono', monospace;
|
|
249
|
+
font-size: 14px; font-weight: 700;
|
|
250
|
+
color: #22d3ee;
|
|
251
|
+
background: #0b1120;
|
|
252
|
+
border: 1px solid #1a2744;
|
|
253
|
+
border-radius: 8px;
|
|
254
|
+
padding: 10px 18px;
|
|
255
|
+
display: inline-block;
|
|
256
|
+
margin-bottom: 6px;
|
|
257
|
+
}
|
|
258
|
+
.cta-sub {
|
|
259
|
+
font-size: 10px; color: #334155;
|
|
260
|
+
}
|
|
261
|
+
</style>
|
|
262
|
+
</head>
|
|
263
|
+
<body>
|
|
264
|
+
<div class="card">
|
|
265
|
+
|
|
266
|
+
<div class="header">
|
|
267
|
+
<div class="logo-claw">🦞</div>
|
|
268
|
+
<div class="logo-text">CLAWCULATOR</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div class="grade-container">
|
|
272
|
+
<div class="grade-ring">
|
|
273
|
+
<div class="grade-letter">${grade}</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="grade-label">${gradeEmoji} cost health</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div class="cost-range">${costEmoji} ${costRange}</div>
|
|
279
|
+
|
|
280
|
+
<div class="divider"></div>
|
|
281
|
+
|
|
282
|
+
<div class="pills">
|
|
283
|
+
${pills.map(p => `<div class="pill"><span>${p.icon}</span>${p.label}</div>`).join('\n ')}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div class="findings">
|
|
287
|
+
${findingSummary.length > 0 ?
|
|
288
|
+
`<div class="findings-badges">${findingSummary.map(f => {
|
|
289
|
+
const c = f.startsWith('🔴') ? '#ef4444' : f.startsWith('🟠') ? '#f97316' : '#eab308';
|
|
290
|
+
return `<div class="findings-badge" style="background:${c}18;color:${c}">${f}</div>`;
|
|
291
|
+
}).join('')}</div>` :
|
|
292
|
+
`<div class="findings-clean">✅ No issues found</div>`
|
|
293
|
+
}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="cta">
|
|
297
|
+
<div class="cta-divider"></div>
|
|
298
|
+
<div class="cta-prompt">Get your OpenClaw cost grade</div>
|
|
299
|
+
<div class="cta-command">npx clawculator --snapshot</div>
|
|
300
|
+
<div class="cta-sub">100% offline · your data never leaves your machine</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
</div>
|
|
304
|
+
</body>
|
|
305
|
+
</html>`;
|
|
306
|
+
|
|
307
|
+
const htmlPath = path.join(outputDir, 'clawculator-snapshot.html');
|
|
308
|
+
fs.writeFileSync(htmlPath, html, 'utf8');
|
|
309
|
+
|
|
310
|
+
// Terminal summary
|
|
311
|
+
console.log(`\n ${gradeEmoji} Grade: \x1b[1m${grade}\x1b[0m`);
|
|
312
|
+
console.log(` ${costEmoji} Cost: ${costRange}`);
|
|
313
|
+
console.log(` 📦 Setup: ${pills.map(p => p.label).join(' · ')}`);
|
|
314
|
+
if (findingSummary.length > 0) {
|
|
315
|
+
console.log(` 🔍 ${findingSummary.join(' · ')}`);
|
|
316
|
+
} else {
|
|
317
|
+
console.log(` ✅ Clean — no issues`);
|
|
318
|
+
}
|
|
319
|
+
console.log('');
|
|
320
|
+
|
|
321
|
+
return { htmlPath, grade, costRange };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = { generateSnapshotCard };
|
|
@@ -18,7 +18,13 @@ function initDB(dbPath) {
|
|
|
18
18
|
try {
|
|
19
19
|
Database = require('better-sqlite3');
|
|
20
20
|
} catch {
|
|
21
|
-
console.error('\x1b[
|
|
21
|
+
console.error('\n\x1b[31m ✗ better-sqlite3 is required for --web\x1b[0m\n');
|
|
22
|
+
console.error(' Install it with:\n');
|
|
23
|
+
console.error(' \x1b[36mnpm install -g better-sqlite3\x1b[0m');
|
|
24
|
+
console.error(' \x1b[90m# or, if installed locally:\x1b[0m');
|
|
25
|
+
console.error(' \x1b[36mcd $(npm root -g)/clawculator && npm install better-sqlite3\x1b[0m\n');
|
|
26
|
+
console.error(' \x1b[90mThis is a native module that compiles on install.\x1b[0m');
|
|
27
|
+
console.error(' \x1b[90mRequires: Node.js 18+, Python 3, and a C++ compiler (Xcode CLI tools on macOS).\x1b[0m\n');
|
|
22
28
|
process.exit(1);
|
|
23
29
|
}
|
|
24
30
|
|
package/src/analyzer.js
CHANGED
|
@@ -5,6 +5,10 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
7
|
// ── Model pricing (per million tokens, input/output) ─────────────
|
|
8
|
+
// Last updated: 2026-02-28 — Update when Anthropic/OpenAI/Google change pricing
|
|
9
|
+
const PRICING_UPDATED = '2026-02-28';
|
|
10
|
+
const PRICING_STALE_DAYS = 60;
|
|
11
|
+
|
|
8
12
|
const MODEL_PRICING = {
|
|
9
13
|
'claude-opus-4-6': { input: 5.00, output: 25.00, label: 'Claude Opus 4.6' },
|
|
10
14
|
'claude-opus-4-5': { input: 5.00, output: 25.00, label: 'Claude Opus 4.5' },
|
|
@@ -109,6 +113,10 @@ const FIXES = {
|
|
|
109
113
|
fix: 'Lower imageMaxDimensionPx to reduce vision token costs — default 1200px is expensive',
|
|
110
114
|
command: 'openclaw config set agents.defaults.imageMaxDimensionPx 800',
|
|
111
115
|
},
|
|
116
|
+
PRICING_STALE: {
|
|
117
|
+
fix: 'Update clawculator to get the latest model pricing data',
|
|
118
|
+
command: 'npm update -g clawculator',
|
|
119
|
+
},
|
|
112
120
|
MULTI_AGENT_PAID: (agentId) => ({
|
|
113
121
|
fix: `Agent "${agentId}" has its own expensive model config — each agent bills independently`,
|
|
114
122
|
command: `Review agents.list[${agentId}].model config and apply same cost rules as primary agent`,
|
|
@@ -503,6 +511,10 @@ function analyzeConfig(configPath) {
|
|
|
503
511
|
|
|
504
512
|
/**
|
|
505
513
|
* Parse a .jsonl session transcript file and sum up real usage/cost data.
|
|
514
|
+
* Version-aware: handles multiple OpenClaw schema formats:
|
|
515
|
+
* - v2026.2.x+: entry.message.usage (standard)
|
|
516
|
+
* - v2026.1.x: entry.usage (legacy)
|
|
517
|
+
* - Anthropic raw: usage.cache_creation_input_tokens / usage.cache_read_input_tokens
|
|
506
518
|
* Returns { input, output, cacheRead, cacheWrite, totalTokens, totalCost, messageCount, model, firstTs, lastTs }
|
|
507
519
|
*/
|
|
508
520
|
function parseTranscript(jsonlPath) {
|
|
@@ -512,6 +524,7 @@ function parseTranscript(jsonlPath) {
|
|
|
512
524
|
|
|
513
525
|
let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, totalTokens = 0, totalCost = 0;
|
|
514
526
|
let messageCount = 0, model = null, firstTs = null, lastTs = null;
|
|
527
|
+
let schemaDetected = null; // track which schema we're seeing
|
|
515
528
|
|
|
516
529
|
for (const line of content.split('\n')) {
|
|
517
530
|
if (!line.trim()) continue;
|
|
@@ -527,23 +540,29 @@ function parseTranscript(jsonlPath) {
|
|
|
527
540
|
}
|
|
528
541
|
|
|
529
542
|
// Only assistant messages with usage blocks have cost data
|
|
530
|
-
|
|
543
|
+
// Some schemas use entry.type === 'message', others use entry.role === 'assistant'
|
|
544
|
+
if (entry.type !== 'message' && entry.role !== 'assistant') continue;
|
|
531
545
|
|
|
532
|
-
// Usage can be
|
|
533
|
-
const u = entry.usage || entry.message?.usage;
|
|
546
|
+
// Usage can be in multiple locations depending on OpenClaw version
|
|
547
|
+
const u = entry.usage || entry.message?.usage || entry.response?.usage;
|
|
534
548
|
if (!u) continue;
|
|
535
549
|
|
|
550
|
+
if (!schemaDetected) {
|
|
551
|
+
schemaDetected = entry.usage ? 'legacy' : entry.message?.usage ? 'standard' : 'response';
|
|
552
|
+
}
|
|
553
|
+
|
|
536
554
|
messageCount++;
|
|
537
555
|
|
|
538
|
-
// Model can be at
|
|
539
|
-
const entryModel = entry.model || entry.message?.model;
|
|
556
|
+
// Model can be at multiple locations
|
|
557
|
+
const entryModel = entry.model || entry.message?.model || entry.response?.model;
|
|
540
558
|
if (entryModel && !model) model = entryModel;
|
|
541
559
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
560
|
+
// Token fields: handle both camelCase (OpenClaw) and snake_case (raw Anthropic API)
|
|
561
|
+
input += u.input || u.input_tokens || 0;
|
|
562
|
+
output += u.output || u.output_tokens || 0;
|
|
563
|
+
cacheRead += u.cacheRead || u.cache_read_input_tokens || 0;
|
|
564
|
+
cacheWrite += u.cacheWrite || u.cache_creation_input_tokens || 0;
|
|
565
|
+
totalTokens += u.totalTokens || ((u.input || u.input_tokens || 0) + (u.output || u.output_tokens || 0)) || 0;
|
|
547
566
|
|
|
548
567
|
// Prefer API-reported cost (already accounts for cache pricing)
|
|
549
568
|
if (u.cost) {
|
|
@@ -557,7 +576,7 @@ function parseTranscript(jsonlPath) {
|
|
|
557
576
|
|
|
558
577
|
if (messageCount === 0) return null;
|
|
559
578
|
|
|
560
|
-
return { input, output, cacheRead, cacheWrite, totalTokens, totalCost, messageCount, model, firstTs, lastTs };
|
|
579
|
+
return { input, output, cacheRead, cacheWrite, totalTokens, totalCost, messageCount, model, firstTs, lastTs, schemaDetected };
|
|
561
580
|
} catch {
|
|
562
581
|
return null;
|
|
563
582
|
}
|
|
@@ -916,6 +935,19 @@ async function runAnalysis({ configPath, sessionsPath, logsDir }) {
|
|
|
916
935
|
|
|
917
936
|
const realCost = sessionResult.totalRealCost || 0;
|
|
918
937
|
|
|
938
|
+
// Check pricing staleness — only affects cost estimates for config findings
|
|
939
|
+
// (actual transcript costs use API-reported cost.total, not the pricing table)
|
|
940
|
+
const pricingAge = Math.floor((Date.now() - new Date(PRICING_UPDATED).getTime()) / 86400000);
|
|
941
|
+
if (pricingAge > PRICING_STALE_DAYS) {
|
|
942
|
+
allFindings.push({
|
|
943
|
+
severity: 'low',
|
|
944
|
+
source: 'pricing',
|
|
945
|
+
title: `Model pricing table is ${pricingAge} days old`,
|
|
946
|
+
detail: `Pricing was last updated ${PRICING_UPDATED}. Actual costs from transcripts are unaffected (they use API-reported totals). Config-based cost estimates (heartbeat bleed, cron projections) may be slightly off. Update: npm update -g clawculator`,
|
|
947
|
+
...FIXES.PRICING_STALE,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
919
951
|
return {
|
|
920
952
|
scannedAt: new Date().toISOString(),
|
|
921
953
|
configPath,
|