clawculator 2.8.2 → 2.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawculator",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
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 || f.message || '').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">
@@ -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 || f.message || '').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">