clawculator 2.6.1 → 2.7.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/webDashboard.js +7 -1
- package/src/analyzer.js +43 -11
- 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
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<img src="logo.png" width="200" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
# Clawculator
|
|
5
|
+
# Clawculator 🦞
|
|
6
6
|
|
|
7
7
|
> **Your friendly penny pincher.**
|
|
8
8
|
|
|
9
|
-
AI cost forensics for OpenClaw and multi-model setups.
|
|
9
|
+
AI cost forensics for OpenClaw and multi-model setups. Real-time browser dashboard. Terminal monitoring. Full offline analysis. Zero AI. Pure deterministic logic.
|
|
10
10
|
|
|
11
11
|
[](https://badge.fury.io/js/clawculator)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -22,22 +22,94 @@ It could be any of these. Clawculator finds all of them — with zero AI, zero g
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx clawculator --web # Browser dashboard at localhost:3457
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it. Pin the tab. Watch your costs in real-time while you work.
|
|
32
|
+
|
|
33
|
+
---
|
|
26
34
|
|
|
27
35
|
## [▶ Live Demo](https://echoudhry.github.io/clawculator)
|
|
28
36
|
|
|
29
|
-
See
|
|
37
|
+
See the full dashboard with simulated data — Pac-Claw, hourly charts, heat map, live feed, and more.
|
|
38
|
+
|
|
39
|
+

|
|
30
40
|
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🦞 The Pac-Claw
|
|
44
|
+
|
|
45
|
+
The dashboard features a Pac-Man-style lobster claw that chomps across the header eating pennies. When an API call comes in, the claw goes into **TURBO mode** — speeds up 3x, glows red, and extra pennies spawn. Because your friendly penny pincher should look the part.
|
|
31
46
|
|
|
32
47
|
---
|
|
33
48
|
|
|
34
|
-
##
|
|
49
|
+
## Modes
|
|
50
|
+
|
|
51
|
+
### `--web` — Browser Dashboard (new in v2.6)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx clawculator --web
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A live-updating browser dashboard at `localhost:3457` powered by SSE (Server-Sent Events) — no polling, instant updates when API calls land.
|
|
58
|
+
|
|
59
|
+
**What you see:**
|
|
60
|
+
- 🔥 **Today's Spend** — big number, real-time, color-coded (green → amber → red)
|
|
61
|
+
- 📊 **Hourly Cost Chart** — bar chart with current hour highlighted
|
|
62
|
+
- 🧠 **Model Mix Donut** — spend breakdown by model (Sonnet vs Opus vs Haiku)
|
|
63
|
+
- ⚡ **Active Sessions** — table sorted by cost with model + message count
|
|
64
|
+
- 🏆 **Costliest Calls** — leaderboard with gold/silver/bronze ranks
|
|
65
|
+
- 📅 **Daily History** — 14-day bar chart with yesterday comparison
|
|
66
|
+
- 🔥 **Spend Heat Map** — 30-day grid (like GitHub contributions, but for spend)
|
|
67
|
+
- 🌊 **Live Feed** — every API call as it happens with slide-in animations
|
|
68
|
+
- 💰 **Burn Rate** — projected monthly cost based on today's velocity
|
|
69
|
+
|
|
70
|
+
**Persistence:** Data is stored in SQLite at `~/.openclaw/clawculator.db`. History builds over time — the longer you run it, the richer your charts get.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx clawculator --web --port=8080 # Custom port
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `--live` — Terminal Dashboard
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx clawculator --live
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A real-time terminal dashboard that watches `.jsonl` transcripts. Perfect for a tmux pane alongside your main session:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
tmux split-window -h "npx clawculator --live"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Press `q` to quit, `r` to force refresh.
|
|
89
|
+
|
|
90
|
+
### `--report` — HTML Report
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx clawculator --report
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Generates a full HTML report and opens it in your browser. Includes findings with terminal-style fix commands, session tables, cost summary, and quick wins.
|
|
97
|
+
|
|
98
|
+
### `--md` — Markdown Report
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx clawculator --md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Structured report your OpenClaw agent can read directly. Drop it in your workspace and ask your agent "what's my cost status?"
|
|
105
|
+
|
|
106
|
+
### Default — Terminal Analysis
|
|
35
107
|
|
|
36
108
|
```bash
|
|
37
109
|
npx clawculator
|
|
38
110
|
```
|
|
39
111
|
|
|
40
|
-
|
|
112
|
+
Color-coded findings by severity with cost estimates and exact fix commands. Today's spend, all-time spend, session breakdown, and actionable quick wins.
|
|
41
113
|
|
|
42
114
|
---
|
|
43
115
|
|
|
@@ -45,7 +117,7 @@ No install. No account. No config. Auto-detects your OpenClaw setup. Full determ
|
|
|
45
117
|
|
|
46
118
|
Clawculator uses **pure switch/case deterministic logic** — no LLM, no Ollama, no model of any kind. Every finding and recommendation is hardcoded. Results are 100% reproducible and non-negotiable.
|
|
47
119
|
|
|
48
|
-
Your `openclaw.json`, session logs, and API keys never leave your machine. There is no server. Disconnect your internet and run it — it works.
|
|
120
|
+
Your `openclaw.json`, session logs, and API keys never leave your machine. The `--web` dashboard runs on `localhost` only. There is no external server. Disconnect your internet and run it — it works.
|
|
49
121
|
|
|
50
122
|
---
|
|
51
123
|
|
|
@@ -62,36 +134,32 @@ Your `openclaw.json`, session logs, and API keys never leave your machine. There
|
|
|
62
134
|
| 🤖 Subagents | maxConcurrent too high — burst multiplier | 🟠 High |
|
|
63
135
|
| 📁 Workspace | Too many root .md files inflating context | 🟡 Medium |
|
|
64
136
|
| 🧠 Memory | memoryFlush on primary model | 🟡 Medium |
|
|
137
|
+
| 💸 Transcript gaps | sessions.json vs actual .jsonl cost discrepancy | 🔴 Critical |
|
|
65
138
|
| ⚙️ Primary model | Cost awareness of chosen model tier | ℹ️ Info |
|
|
66
139
|
|
|
67
140
|
---
|
|
68
141
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npx clawculator # Terminal analysis (default)
|
|
73
|
-
npx clawculator --md # Markdown report (readable by your AI agent)
|
|
74
|
-
npx clawculator --report # Visual HTML dashboard
|
|
75
|
-
npx clawculator --json # JSON for piping into other tools
|
|
76
|
-
npx clawculator --md --out=~/cost.md # Custom output path
|
|
77
|
-
npx clawculator --config=/path/to/openclaw.json
|
|
78
|
-
npx clawculator --help
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
142
|
+
## Transcript parsing
|
|
82
143
|
|
|
83
|
-
|
|
144
|
+
Clawculator reads `.jsonl` transcript files directly from `~/.openclaw/agents/*/sessions/` to calculate **actual API spend** — not what `sessions.json` reports (which can be 1000x+ under-reported).
|
|
84
145
|
|
|
85
|
-
|
|
146
|
+
The `--web` and `--live` modes watch these files in real-time, tailing new lines as they're appended. Every API call shows up in your dashboard the moment it happens.
|
|
86
147
|
|
|
87
|
-
|
|
148
|
+
---
|
|
88
149
|
|
|
89
|
-
|
|
150
|
+
## All flags
|
|
90
151
|
|
|
91
|
-
**JSON (`--json`)** — machine-readable, pipeable:
|
|
92
152
|
```bash
|
|
93
|
-
npx clawculator
|
|
94
|
-
npx clawculator --
|
|
153
|
+
npx clawculator # Terminal analysis (default)
|
|
154
|
+
npx clawculator --web # Browser dashboard (localhost:3457)
|
|
155
|
+
npx clawculator --web --port=8080 # Custom port
|
|
156
|
+
npx clawculator --live # Real-time terminal dashboard
|
|
157
|
+
npx clawculator --report # Visual HTML report
|
|
158
|
+
npx clawculator --md # Markdown report
|
|
159
|
+
npx clawculator --json # JSON for piping
|
|
160
|
+
npx clawculator --md --out=~/cost.md # Custom output path
|
|
161
|
+
npx clawculator --config=/path/to/openclaw.json
|
|
162
|
+
npx clawculator --help
|
|
95
163
|
```
|
|
96
164
|
|
|
97
165
|
---
|
|
@@ -120,18 +188,15 @@ curl -o ~/clawd/skills/clawculator/SKILL.md \
|
|
|
120
188
|
https://raw.githubusercontent.com/echoudhry/clawculator/main/skills/clawculator/SKILL.md
|
|
121
189
|
```
|
|
122
190
|
|
|
123
|
-
Start a new session to pick it up.
|
|
124
|
-
|
|
125
191
|
---
|
|
126
192
|
|
|
127
|
-
##
|
|
128
|
-
|
|
129
|
-
Every recommendation is a hardcoded switch/case — not generated by an AI. This means:
|
|
193
|
+
## Tech stack
|
|
130
194
|
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
195
|
+
- **Node.js** — zero dependencies for core analysis
|
|
196
|
+
- **better-sqlite3** — optional, powers `--web` persistence and historical charts
|
|
197
|
+
- **SSE** — Server-Sent Events for real-time browser updates (no WebSocket overhead)
|
|
198
|
+
- **fs.watch** — native file watching for `.jsonl` transcript tailing
|
|
199
|
+
- Pure CSS animations — no React, no build step, no bundler
|
|
135
200
|
|
|
136
201
|
---
|
|
137
202
|
|
package/bin/clawculator.js
CHANGED
|
@@ -15,6 +15,7 @@ const flags = {
|
|
|
15
15
|
md: args.includes('--md'),
|
|
16
16
|
live: args.includes('--live'),
|
|
17
17
|
web: args.includes('--web'),
|
|
18
|
+
prune: args.includes('--prune'),
|
|
18
19
|
help: args.includes('--help') || args.includes('-h'),
|
|
19
20
|
config: args.find(a => a.startsWith('--config='))?.split('=')[1],
|
|
20
21
|
out: args.find(a => a.startsWith('--out='))?.split('=')[1],
|
|
@@ -45,6 +46,8 @@ Options:
|
|
|
45
46
|
--md Save markdown report to ./clawculator-report.md
|
|
46
47
|
--json Output raw JSON
|
|
47
48
|
--out=PATH Custom output path for --md or --report
|
|
49
|
+
--port=PORT Custom port for --web (default: 3457)
|
|
50
|
+
--prune Clean up SQLite database (prune events > 14d, snapshots > 1y)
|
|
48
51
|
--config=PATH Path to openclaw.json (auto-detected by default)
|
|
49
52
|
--help, -h Show this help
|
|
50
53
|
|
|
@@ -69,6 +72,33 @@ async function main() {
|
|
|
69
72
|
|
|
70
73
|
const openclawHome = process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
|
|
71
74
|
|
|
75
|
+
if (flags.prune) {
|
|
76
|
+
const dbPath = path.join(openclawHome, 'clawculator.db');
|
|
77
|
+
if (!fs.existsSync(dbPath)) {
|
|
78
|
+
console.log('\x1b[90mNo database found at\x1b[0m', dbPath);
|
|
79
|
+
console.log('\x1b[90mRun --web first to create the database.\x1b[0m');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
let Database;
|
|
83
|
+
try { Database = require('better-sqlite3'); } catch {
|
|
84
|
+
console.error('\x1b[31mError:\x1b[0m better-sqlite3 required for --prune. Run: npm install better-sqlite3');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const db = new Database(dbPath);
|
|
88
|
+
const eventsBefore = db.prepare('SELECT COUNT(*) as c FROM events').get().c;
|
|
89
|
+
const snapsBefore = db.prepare('SELECT COUNT(*) as c FROM daily_snapshots').get().c;
|
|
90
|
+
db.exec(`DELETE FROM events WHERE timestamp < datetime('now', '-14 days')`);
|
|
91
|
+
db.exec(`DELETE FROM daily_snapshots WHERE date < date('now', '-365 days')`);
|
|
92
|
+
db.exec('VACUUM');
|
|
93
|
+
const eventsAfter = db.prepare('SELECT COUNT(*) as c FROM events').get().c;
|
|
94
|
+
const snapsAfter = db.prepare('SELECT COUNT(*) as c FROM daily_snapshots').get().c;
|
|
95
|
+
db.close();
|
|
96
|
+
console.log(`\x1b[32m✓ Pruned database:\x1b[0m ${dbPath}`);
|
|
97
|
+
console.log(` Events: ${eventsBefore} → ${eventsAfter} (removed ${eventsBefore - eventsAfter})`);
|
|
98
|
+
console.log(` Snapshots: ${snapsBefore} → ${snapsAfter} (removed ${snapsBefore - snapsAfter})`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
if (flags.web) {
|
|
73
103
|
const { startWebDashboard } = require('../src/webDashboard');
|
|
74
104
|
startWebDashboard({ openclawHome, port: flags.port });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawculator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "AI cost forensics for OpenClaw and multi-model setups. Your friendly penny pincher. 100% offline. Zero AI. Pure deterministic logic.",
|
|
5
5
|
"main": "src/analyzer.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/clawculator.js",
|
|
11
|
-
"test": "node
|
|
11
|
+
"test": "node test/run.js"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"openclaw",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=18.0.0"
|
|
32
32
|
},
|
|
33
|
+
"dependencies": {},
|
|
33
34
|
"optionalDependencies": {
|
|
34
|
-
"better-sqlite3": "^11.
|
|
35
|
+
"better-sqlite3": "^11.0.0"
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -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,
|
|
@@ -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,
|
package/src/webDashboard.js
CHANGED
|
@@ -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
|
|