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 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. One command. Full analysis. 100% offline. Zero AI. Pure deterministic logic.
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
  [![npm version](https://badge.fury.io/js/clawculator.svg)](https://badge.fury.io/js/clawculator)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ![Clawculator Report](report-preview.png)
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 it run against a real configfindings, fix commands, cost exposure, session breakdown.
37
+ See the full dashboard with simulated dataPac-Claw, hourly charts, heat map, live feed, and more.
38
+
39
+ ![Clawculator Web Dashboard](web-dashboard.png)
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
- ## One command
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
- No install. No account. No config. Auto-detects your OpenClaw setup. Full deterministic report in seconds.
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
- ## Usage
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
- ## Output formats
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
- **Terminal** color-coded findings by severity with cost estimates and exact fix commands.
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
- **Markdown (`--md`)** — structured report your OpenClaw agent can read directly. Drop it in your workspace and ask your agent "what's my cost status?" It reads `clawculator-report.md` and answers.
148
+ ---
88
149
 
89
- **HTML (`--report`)** — visual dashboard with session breakdown table, cost exposure banner, opens in browser locally.
150
+ ## All flags
90
151
 
91
- **JSON (`--json`)** — machine-readable, pipeable:
92
152
  ```bash
93
- npx clawculator --json | jq '.summary'
94
- npx clawculator --json > cost-report.json
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
- ## Why deterministic?
128
-
129
- Every recommendation is a hardcoded switch/case — not generated by an AI. This means:
193
+ ## Tech stack
130
194
 
131
- - Results are identical every time for the same input
132
- - No hallucinations, no surprises
133
- - Works completely offline with no model dependency
134
- - Fastanalysis runs in under a second
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
 
@@ -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.6.1",
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 -e \"const {runAnalysis} = require('./src/analyzer'); runAnalysis({configPath:'~/.openclaw/openclaw.json',sessionsPath:'',logsDir:''}).then(r => console.log('OK:', r.summary))\""
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.10.0"
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
- if (entry.type !== 'message') continue;
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 at entry.usage (some formats) or entry.message.usage (standard format)
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 entry.model or entry.message.model
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
- input += u.input || 0;
543
- output += u.output || 0;
544
- cacheRead += u.cacheRead || 0;
545
- cacheWrite += u.cacheWrite || 0;
546
- totalTokens += u.totalTokens || 0;
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[31mError:\x1b[0m better-sqlite3 not installed. Run: npm install better-sqlite3');
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
- if (entry.type !== 'message') continue;
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 at entry.usage (some formats) or entry.message.usage (standard format)
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 entry.model or entry.message.model
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
- input += u.input || 0;
543
- output += u.output || 0;
544
- cacheRead += u.cacheRead || 0;
545
- cacheWrite += u.cacheWrite || 0;
546
- totalTokens += u.totalTokens || 0;
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[31mError:\x1b[0m better-sqlite3 not installed. Run: npm install better-sqlite3');
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