clawculator 2.8.1 → 2.8.3

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
@@ -25,11 +25,10 @@ It could be any of these. Clawculator finds all of them — with zero AI, zero g
25
25
  ## Quick start
26
26
 
27
27
  ```bash
28
- npx clawculator --web # Browser dashboard at localhost:3457
28
+ npx clawculator --snapshot # Get your cost grade — screenshot & share
29
+ npx clawculator --web # Browser dashboard at localhost:3457
29
30
  ```
30
31
 
31
- That's it. Pin the tab. Watch your costs in real-time while you work.
32
-
33
32
  ---
34
33
 
35
34
  ## [▶ Live Demo](https://echoudhry.github.io/clawculator)
@@ -48,6 +47,41 @@ The dashboard features a Pac-Man-style lobster claw that chomps across the heade
48
47
 
49
48
  ## Modes
50
49
 
50
+ ### `--snapshot` — Shareable Grade Card (new in v2.8)
51
+
52
+ ```bash
53
+ npx clawculator --snapshot
54
+ ```
55
+
56
+ Grades your setup **A+ to D**, shows your daily cost range, and displays your setup complexity — all in a screenshot-ready terminal card. No exact dollar amounts or session names are exposed. Built for sharing.
57
+
58
+ ```
59
+ ╔══════════════════════════════════════════════╗
60
+ ║ ║
61
+ ║ 🦞 C L A W C U L A T O R ║
62
+ ║ ║
63
+ ║ ┌─────────┐ ║
64
+ ║ │ A+ │ ║
65
+ ║ └─────────┘ ║
66
+ ║ cost health grade ║
67
+ ║ ║
68
+ ║ 💲 Under $1/day ║
69
+ ║ ║
70
+ ║ 📱 2 channels 🔧 4 skills ⏰ 1 cron ║
71
+ ║ 💬 6 sessions 🧠 2 models ⚡ 85% cache ║
72
+ ║ ║
73
+ ║ ✅ No issues found ║
74
+ ║ ║
75
+ ║ Get your OpenClaw cost grade ║
76
+ ║ npx clawculator --snapshot ║
77
+ ║ ║
78
+ ╚══════════════════════════════════════════════╝
79
+ ```
80
+
81
+ Screenshot it. Post it. What's your grade?
82
+
83
+ Also works as an OpenClaw skill — type `snapshot` or `what's my grade` in any channel.
84
+
51
85
  ### `--web` — Browser Dashboard (new in v2.6)
52
86
 
53
87
  ```bash
@@ -151,12 +185,14 @@ The `--web` and `--live` modes watch these files in real-time, tailing new lines
151
185
 
152
186
  ```bash
153
187
  npx clawculator # Terminal analysis (default)
188
+ npx clawculator --snapshot # Shareable grade card (screenshot & post!)
154
189
  npx clawculator --web # Browser dashboard (localhost:3457)
155
190
  npx clawculator --web --port=8080 # Custom port
156
191
  npx clawculator --live # Real-time terminal dashboard
157
192
  npx clawculator --report # Visual HTML report
158
193
  npx clawculator --md # Markdown report
159
194
  npx clawculator --json # JSON for piping
195
+ npx clawculator --prune # Clean up SQLite database
160
196
  npx clawculator --md --out=~/cost.md # Custom output path
161
197
  npx clawculator --config=/path/to/openclaw.json
162
198
  npx clawculator --help
@@ -181,6 +217,11 @@ clawculator
181
217
 
182
218
  Your agent runs the analysis and returns the full markdown report directly in chat.
183
219
 
220
+ For the shareable grade card, type:
221
+ ```
222
+ snapshot
223
+ ```
224
+
184
225
  **Or install manually** into your workspace:
185
226
  ```bash
186
227
  mkdir -p ~/clawd/skills/clawculator
@@ -16,6 +16,7 @@ const flags = {
16
16
  live: args.includes('--live'),
17
17
  web: args.includes('--web'),
18
18
  prune: args.includes('--prune'),
19
+ snapshot: args.includes('--snapshot'),
19
20
  help: args.includes('--help') || args.includes('-h'),
20
21
  config: args.find(a => a.startsWith('--config='))?.split('=')[1],
21
22
  out: args.find(a => a.startsWith('--out='))?.split('=')[1],
@@ -43,6 +44,7 @@ Options:
43
44
  --live Real-time cost dashboard in terminal (great for tmux)
44
45
  --web Browser dashboard at localhost:3457 (pin the tab!)
45
46
  --report Generate HTML report and open in browser
47
+ --snapshot Generate a shareable cost snapshot card (screenshot & post!)
46
48
  --md Save markdown report to ./clawculator-report.md
47
49
  --json Output raw JSON
48
50
  --out=PATH Custom output path for --md or --report
@@ -54,6 +56,7 @@ Options:
54
56
  Examples:
55
57
  npx clawculator # Terminal analysis
56
58
  npx clawculator --web # Browser dashboard (localhost:3457)
59
+ npx clawculator --snapshot # Shareable cost card (screenshot & post!)
57
60
  npx clawculator --live # Real-time terminal dashboard
58
61
  npx clawculator --md # Markdown report (readable by your AI agent)
59
62
  npx clawculator --report # Visual HTML dashboard
@@ -164,6 +167,19 @@ async function main() {
164
167
  process.exit(0);
165
168
  }
166
169
 
170
+ if (flags.snapshot) {
171
+ const outDir = flags.out || process.cwd();
172
+ const { generateSnapshotCard } = require('../src/snapshotCard');
173
+ const result = generateSnapshotCard(analysis, outDir);
174
+ const { exec } = require('child_process');
175
+ exec(`open "${result.htmlPath}" 2>/dev/null || xdg-open "${result.htmlPath}" 2>/dev/null`, () => {
176
+ process.exit(0);
177
+ });
178
+ // Don't exit immediately — give exec time to open browser
179
+ setTimeout(() => process.exit(0), 2000);
180
+ return;
181
+ }
182
+
167
183
  generateTerminalReport(analysis);
168
184
  console.log('\x1b[90m────────────────────────────────────────────────────\x1b[0m');
169
185
  console.log('\x1b[36mClawculator\x1b[0m · github.com/echoudhry/clawculator · Your friendly penny pincher.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawculator",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
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": {
@@ -17,18 +17,27 @@ function generateSnapshotCard(analysis, outputDir) {
17
17
  const mediums = s.medium || 0;
18
18
  const totalFindings = criticals + highs + mediums;
19
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
- }
20
+ // ── Filter findings by type ────────────────────────────
21
+ // For the grade, only count CONFIG findings (things a new user would inherit)
22
+ // Exclude: historical session costs, untracked sessions, cache totals, cost gaps
23
+ // These are noise for "what does it cost to run this setup?"
24
+ const configFindings = (analysis.findings || []).filter(f => {
25
+ const t = (f.title || '').toLowerCase();
26
+ const src = (f.source || '').toLowerCase();
27
+ // Skip historical/session findings
28
+ if (t.includes('orphaned session')) return false;
29
+ if (t.includes('untracked session')) return false;
30
+ if (t.includes('prompt caching added')) return false;
31
+ if (t.includes('real cost') && t.includes('higher than')) return false;
32
+ if (t.includes('>50k tokens')) return false;
33
+ if (t.includes('pricing table')) return false;
34
+ // Keep config findings (heartbeat, whatsapp, hooks, crons, models, vision, memory)
35
+ return true;
36
+ });
37
+
38
+ const cfgCriticals = configFindings.filter(f => f.severity === 'critical').length;
39
+ const cfgHighs = configFindings.filter(f => f.severity === 'high').length;
40
+ const cfgMediums = configFindings.filter(f => f.severity === 'medium').length;
32
41
 
33
42
  // ── Cost range (bucketed, not exact) ───────────────────
34
43
  const todayCost = s.todayCost || 0;
@@ -42,6 +51,38 @@ function generateSnapshotCard(analysis, outputDir) {
42
51
  else if (todayCost < 50) { costRange = '$25–50/day'; costEmoji = '🔥'; }
43
52
  else { costRange = '$50+/day'; costEmoji = '🚨'; }
44
53
 
54
+ // ── Compute grade ──────────────────────────────────────
55
+ // Grade = "how well is this SETUP configured for cost efficiency?"
56
+ // Only counts config findings, not historical session data
57
+ let grade, gradeColor, gradeGlow, gradeEmoji;
58
+
59
+ let score = (cfgCriticals * 5) + (cfgHighs * 2) + (cfgMediums * 0.5);
60
+
61
+ // Cost bonus: low daily spend is the whole point
62
+ if (todayCost < 1) score -= 4;
63
+ else if (todayCost < 5) score -= 3;
64
+ else if (todayCost < 10) score -= 1;
65
+ else if (todayCost > 50) score += 3;
66
+
67
+ // No criticals bonus
68
+ if (cfgCriticals === 0) score -= 2;
69
+
70
+ if (score < 0) score = 0;
71
+
72
+ if (score === 0) {
73
+ grade = 'A+'; gradeColor = '#22c55e'; gradeGlow = 'rgba(34,197,94,0.3)'; gradeEmoji = '🏆';
74
+ } else if (score <= 2) {
75
+ grade = 'A'; gradeColor = '#22c55e'; gradeGlow = 'rgba(34,197,94,0.2)'; gradeEmoji = '✅';
76
+ } else if (score <= 5) {
77
+ grade = 'B+'; gradeColor = '#84cc16'; gradeGlow = 'rgba(132,204,22,0.2)'; gradeEmoji = '👍';
78
+ } else if (score <= 8) {
79
+ grade = 'B'; gradeColor = '#f59e0b'; gradeGlow = 'rgba(245,158,11,0.2)'; gradeEmoji = '👍';
80
+ } else if (score <= 12) {
81
+ grade = 'C'; gradeColor = '#f97316'; gradeGlow = 'rgba(249,115,22,0.2)'; gradeEmoji = '⚠️';
82
+ } else {
83
+ grade = 'D'; gradeColor = '#ef4444'; gradeGlow = 'rgba(239,68,68,0.2)'; gradeEmoji = '🔥';
84
+ }
85
+
45
86
  // ── Setup complexity from config ───────────────────────
46
87
  const config = analysis.config || {};
47
88
 
@@ -66,7 +107,8 @@ function generateSnapshotCard(analysis, outputDir) {
66
107
  const modelCount = modelSet.size || 1;
67
108
 
68
109
  const totalTokens = s.totalTokensFound || 1;
69
- const cacheEfficiency = Math.round((s.totalCacheRead || 0) / totalTokens * 100);
110
+ const totalWithCache = totalTokens + (s.totalCacheRead || 0) + (s.totalCacheWrite || 0);
111
+ const cacheEfficiency = Math.min(99, Math.round((s.totalCacheRead || 0) / totalWithCache * 100));
70
112
 
71
113
  // ── Build stat pills ──────────────────────────────────
72
114
  const pills = [];
@@ -79,11 +121,11 @@ function generateSnapshotCard(analysis, outputDir) {
79
121
  pills.push({ icon: '🧠', label: `${modelCount} model${modelCount>1?'s':''}` });
80
122
  if (cacheEfficiency > 0) pills.push({ icon: '⚡', label: `${cacheEfficiency}% cache` });
81
123
 
82
- // ── Findings summary ──────────────────────────────────
124
+ // ── Findings summary (config only) ─────────────────────
83
125
  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`);
126
+ if (cfgCriticals > 0) findingSummary.push(`🔴 ${cfgCriticals} critical`);
127
+ if (cfgHighs > 0) findingSummary.push(`🟠 ${cfgHighs} high`);
128
+ if (cfgMediums > 0) findingSummary.push(`🟡 ${cfgMediums} medium`);
87
129
 
88
130
  const html = `<!DOCTYPE html>
89
131
  <html lang="en">
@@ -307,16 +349,52 @@ function generateSnapshotCard(analysis, outputDir) {
307
349
  const htmlPath = path.join(outputDir, 'clawculator-snapshot.html');
308
350
  fs.writeFileSync(htmlPath, html, 'utf8');
309
351
 
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('');
352
+ // ── Terminal card (screenshot-ready) ───────────────────
353
+ const C = '\x1b[36m'; // cyan
354
+ const G = gradeColor === '#22c55e' ? '\x1b[32m' : gradeColor === '#f59e0b' ? '\x1b[33m' : gradeColor === '#f97316' ? '\x1b[33m' : '\x1b[31m';
355
+ const D = '\x1b[90m'; // dim
356
+ const B = '\x1b[1m'; // bold
357
+ const R = '\x1b[0m'; // reset
358
+ const W = '\x1b[37m'; // white
359
+
360
+ const pillStr = pills.map(p => `${p.icon} ${p.label}`).join(' ');
361
+ const findStr = findingSummary.length > 0
362
+ ? findingSummary.join(' ')
363
+ : '✅ No issues found';
364
+
365
+ const lines = [
366
+ ``,
367
+ `${D} ╔══════════════════════════════════════════════╗${R}`,
368
+ `${D} ║${R} ${D}║${R}`,
369
+ `${D} ║${R} ${C}🦞 C L A W C U L A T O R${R} ${D}║${R}`,
370
+ `${D} ║${R} ${D}║${R}`,
371
+ `${D} ║${R} ${G}${B}┌─────────┐${R} ${D}║${R}`,
372
+ `${D} ║${R} ${G}${B}│ ${grade.padStart(3)} │${R} ${D}║${R}`,
373
+ `${D} ║${R} ${G}${B}└─────────┘${R} ${D}║${R}`,
374
+ `${D} ║${R} ${D}cost health grade${R} ${D}║${R}`,
375
+ `${D} ║${R} ${D}║${R}`,
376
+ `${D} ║${R} ${W}${B}${costEmoji} ${costRange}${R}`,
377
+ `${D} ║${R} ${D}║${R}`,
378
+ `${D} ║${R} ${D}──────────────────────────────────────────${R} ${D}║${R}`,
379
+ `${D} ║${R} ${D}║${R}`,
380
+ `${D} ║${R} ${pillStr}`,
381
+ `${D} ║${R} ${D}║${R}`,
382
+ `${D} ║${R} ${findStr}`,
383
+ `${D} ║${R} ${D}║${R}`,
384
+ `${D} ║${R} ${D}──────────────────────────────────────────${R} ${D}║${R}`,
385
+ `${D} ║${R} ${D}║${R}`,
386
+ `${D} ║${R} ${D}Get your OpenClaw cost grade${R} ${D}║${R}`,
387
+ `${D} ║${R} ${C}${B}npx clawculator --snapshot${R} ${D}║${R}`,
388
+ `${D} ║${R} ${D}║${R}`,
389
+ `${D} ╚══════════════════════════════════════════════╝${R}`,
390
+ ``,
391
+ ];
392
+
393
+ console.log(lines.join('\n'));
394
+
395
+ // Also mention the HTML file
396
+ console.log(` ${D}HTML card saved: ${htmlPath}${R}`);
397
+ console.log(` ${D}Screenshot this terminal or open the HTML — both work!${R}\n`);
320
398
 
321
399
  return { htmlPath, grade, costRange };
322
400
  }
@@ -17,18 +17,27 @@ function generateSnapshotCard(analysis, outputDir) {
17
17
  const mediums = s.medium || 0;
18
18
  const totalFindings = criticals + highs + mediums;
19
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
- }
20
+ // ── Filter findings by type ────────────────────────────
21
+ // For the grade, only count CONFIG findings (things a new user would inherit)
22
+ // Exclude: historical session costs, untracked sessions, cache totals, cost gaps
23
+ // These are noise for "what does it cost to run this setup?"
24
+ const configFindings = (analysis.findings || []).filter(f => {
25
+ const t = (f.title || '').toLowerCase();
26
+ const src = (f.source || '').toLowerCase();
27
+ // Skip historical/session findings
28
+ if (t.includes('orphaned session')) return false;
29
+ if (t.includes('untracked session')) return false;
30
+ if (t.includes('prompt caching added')) return false;
31
+ if (t.includes('real cost') && t.includes('higher than')) return false;
32
+ if (t.includes('>50k tokens')) return false;
33
+ if (t.includes('pricing table')) return false;
34
+ // Keep config findings (heartbeat, whatsapp, hooks, crons, models, vision, memory)
35
+ return true;
36
+ });
37
+
38
+ const cfgCriticals = configFindings.filter(f => f.severity === 'critical').length;
39
+ const cfgHighs = configFindings.filter(f => f.severity === 'high').length;
40
+ const cfgMediums = configFindings.filter(f => f.severity === 'medium').length;
32
41
 
33
42
  // ── Cost range (bucketed, not exact) ───────────────────
34
43
  const todayCost = s.todayCost || 0;
@@ -42,6 +51,38 @@ function generateSnapshotCard(analysis, outputDir) {
42
51
  else if (todayCost < 50) { costRange = '$25–50/day'; costEmoji = '🔥'; }
43
52
  else { costRange = '$50+/day'; costEmoji = '🚨'; }
44
53
 
54
+ // ── Compute grade ──────────────────────────────────────
55
+ // Grade = "how well is this SETUP configured for cost efficiency?"
56
+ // Only counts config findings, not historical session data
57
+ let grade, gradeColor, gradeGlow, gradeEmoji;
58
+
59
+ let score = (cfgCriticals * 5) + (cfgHighs * 2) + (cfgMediums * 0.5);
60
+
61
+ // Cost bonus: low daily spend is the whole point
62
+ if (todayCost < 1) score -= 4;
63
+ else if (todayCost < 5) score -= 3;
64
+ else if (todayCost < 10) score -= 1;
65
+ else if (todayCost > 50) score += 3;
66
+
67
+ // No criticals bonus
68
+ if (cfgCriticals === 0) score -= 2;
69
+
70
+ if (score < 0) score = 0;
71
+
72
+ if (score === 0) {
73
+ grade = 'A+'; gradeColor = '#22c55e'; gradeGlow = 'rgba(34,197,94,0.3)'; gradeEmoji = '🏆';
74
+ } else if (score <= 2) {
75
+ grade = 'A'; gradeColor = '#22c55e'; gradeGlow = 'rgba(34,197,94,0.2)'; gradeEmoji = '✅';
76
+ } else if (score <= 5) {
77
+ grade = 'B+'; gradeColor = '#84cc16'; gradeGlow = 'rgba(132,204,22,0.2)'; gradeEmoji = '👍';
78
+ } else if (score <= 8) {
79
+ grade = 'B'; gradeColor = '#f59e0b'; gradeGlow = 'rgba(245,158,11,0.2)'; gradeEmoji = '👍';
80
+ } else if (score <= 12) {
81
+ grade = 'C'; gradeColor = '#f97316'; gradeGlow = 'rgba(249,115,22,0.2)'; gradeEmoji = '⚠️';
82
+ } else {
83
+ grade = 'D'; gradeColor = '#ef4444'; gradeGlow = 'rgba(239,68,68,0.2)'; gradeEmoji = '🔥';
84
+ }
85
+
45
86
  // ── Setup complexity from config ───────────────────────
46
87
  const config = analysis.config || {};
47
88
 
@@ -66,7 +107,8 @@ function generateSnapshotCard(analysis, outputDir) {
66
107
  const modelCount = modelSet.size || 1;
67
108
 
68
109
  const totalTokens = s.totalTokensFound || 1;
69
- const cacheEfficiency = Math.round((s.totalCacheRead || 0) / totalTokens * 100);
110
+ const totalWithCache = totalTokens + (s.totalCacheRead || 0) + (s.totalCacheWrite || 0);
111
+ const cacheEfficiency = Math.min(99, Math.round((s.totalCacheRead || 0) / totalWithCache * 100));
70
112
 
71
113
  // ── Build stat pills ──────────────────────────────────
72
114
  const pills = [];
@@ -79,11 +121,11 @@ function generateSnapshotCard(analysis, outputDir) {
79
121
  pills.push({ icon: '🧠', label: `${modelCount} model${modelCount>1?'s':''}` });
80
122
  if (cacheEfficiency > 0) pills.push({ icon: '⚡', label: `${cacheEfficiency}% cache` });
81
123
 
82
- // ── Findings summary ──────────────────────────────────
124
+ // ── Findings summary (config only) ─────────────────────
83
125
  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`);
126
+ if (cfgCriticals > 0) findingSummary.push(`🔴 ${cfgCriticals} critical`);
127
+ if (cfgHighs > 0) findingSummary.push(`🟠 ${cfgHighs} high`);
128
+ if (cfgMediums > 0) findingSummary.push(`🟡 ${cfgMediums} medium`);
87
129
 
88
130
  const html = `<!DOCTYPE html>
89
131
  <html lang="en">
@@ -307,16 +349,52 @@ function generateSnapshotCard(analysis, outputDir) {
307
349
  const htmlPath = path.join(outputDir, 'clawculator-snapshot.html');
308
350
  fs.writeFileSync(htmlPath, html, 'utf8');
309
351
 
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('');
352
+ // ── Terminal card (screenshot-ready) ───────────────────
353
+ const C = '\x1b[36m'; // cyan
354
+ const G = gradeColor === '#22c55e' ? '\x1b[32m' : gradeColor === '#f59e0b' ? '\x1b[33m' : gradeColor === '#f97316' ? '\x1b[33m' : '\x1b[31m';
355
+ const D = '\x1b[90m'; // dim
356
+ const B = '\x1b[1m'; // bold
357
+ const R = '\x1b[0m'; // reset
358
+ const W = '\x1b[37m'; // white
359
+
360
+ const pillStr = pills.map(p => `${p.icon} ${p.label}`).join(' ');
361
+ const findStr = findingSummary.length > 0
362
+ ? findingSummary.join(' ')
363
+ : '✅ No issues found';
364
+
365
+ const lines = [
366
+ ``,
367
+ `${D} ╔══════════════════════════════════════════════╗${R}`,
368
+ `${D} ║${R} ${D}║${R}`,
369
+ `${D} ║${R} ${C}🦞 C L A W C U L A T O R${R} ${D}║${R}`,
370
+ `${D} ║${R} ${D}║${R}`,
371
+ `${D} ║${R} ${G}${B}┌─────────┐${R} ${D}║${R}`,
372
+ `${D} ║${R} ${G}${B}│ ${grade.padStart(3)} │${R} ${D}║${R}`,
373
+ `${D} ║${R} ${G}${B}└─────────┘${R} ${D}║${R}`,
374
+ `${D} ║${R} ${D}cost health grade${R} ${D}║${R}`,
375
+ `${D} ║${R} ${D}║${R}`,
376
+ `${D} ║${R} ${W}${B}${costEmoji} ${costRange}${R}`,
377
+ `${D} ║${R} ${D}║${R}`,
378
+ `${D} ║${R} ${D}──────────────────────────────────────────${R} ${D}║${R}`,
379
+ `${D} ║${R} ${D}║${R}`,
380
+ `${D} ║${R} ${pillStr}`,
381
+ `${D} ║${R} ${D}║${R}`,
382
+ `${D} ║${R} ${findStr}`,
383
+ `${D} ║${R} ${D}║${R}`,
384
+ `${D} ║${R} ${D}──────────────────────────────────────────${R} ${D}║${R}`,
385
+ `${D} ║${R} ${D}║${R}`,
386
+ `${D} ║${R} ${D}Get your OpenClaw cost grade${R} ${D}║${R}`,
387
+ `${D} ║${R} ${C}${B}npx clawculator --snapshot${R} ${D}║${R}`,
388
+ `${D} ║${R} ${D}║${R}`,
389
+ `${D} ╚══════════════════════════════════════════════╝${R}`,
390
+ ``,
391
+ ];
392
+
393
+ console.log(lines.join('\n'));
394
+
395
+ // Also mention the HTML file
396
+ console.log(` ${D}HTML card saved: ${htmlPath}${R}`);
397
+ console.log(` ${D}Screenshot this terminal or open the HTML — both work!${R}\n`);
320
398
 
321
399
  return { htmlPath, grade, costRange };
322
400
  }