clawculator 2.0.0 โ 2.1.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 +38 -0
- package/package.json +5 -12
- package/skills/clawculator/README.md +153 -0
- package/skills/clawculator/SKILL.md +57 -0
- package/skills/clawculator/analyzer.js +598 -0
- package/skills/clawculator/htmlReport.js +186 -0
- package/skills/clawculator/mdReport.js +147 -0
- package/skills/clawculator/reporter.js +139 -0
- package/skills/clawculator/run.js +102 -0
- package/logo.png +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
function severityColor(severity) {
|
|
8
|
+
return { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#38bdf8', info: '#22c55e' }[severity] || '#6b7280';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function severityBg(severity) {
|
|
12
|
+
return { critical: '#fef2f2', high: '#fff7ed', medium: '#fefce8', low: '#f0f9ff', info: '#f0fdf4' }[severity] || '#f9fafb';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function severityIcon(severity) {
|
|
16
|
+
return { critical: '๐ด', high: '๐ ', medium: '๐ก', low: '๐ต', info: 'โ
' }[severity] || 'โช';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function relativeAge(ageMs) {
|
|
20
|
+
if (!ageMs) return 'unknown';
|
|
21
|
+
const s = Math.floor(ageMs / 1000);
|
|
22
|
+
if (s < 60) return `${s}s ago`;
|
|
23
|
+
const m = Math.floor(s / 60);
|
|
24
|
+
if (m < 60) return `${m}m ago`;
|
|
25
|
+
const h = Math.floor(m / 60);
|
|
26
|
+
if (h < 24) return `${h}h ago`;
|
|
27
|
+
const d = Math.floor(h / 24);
|
|
28
|
+
if (d < 30) return `${d}d ago`;
|
|
29
|
+
return `${Math.floor(d / 30)}mo ago`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SOURCE_LABELS = {
|
|
33
|
+
heartbeat: '๐ Heartbeat', hooks: '๐ช Hooks', whatsapp: '๐ฑ WhatsApp',
|
|
34
|
+
subagents: '๐ค Subagents', skills: '๐ง Skills', memory: '๐ง Memory',
|
|
35
|
+
primary_model: 'โ๏ธ Primary Model', sessions: '๐ฌ Sessions',
|
|
36
|
+
workspace: '๐ Workspace', config: '๐ Config',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async function generateHTMLReport(analysis, outPath) {
|
|
40
|
+
const { summary, findings, sessions } = analysis;
|
|
41
|
+
const bleed = summary.estimatedMonthlyBleed;
|
|
42
|
+
|
|
43
|
+
const findingCards = findings.map(f => `
|
|
44
|
+
<div class="finding" style="border-left: 4px solid ${severityColor(f.severity)}; background: ${severityBg(f.severity)}; padding: 16px; margin-bottom: 12px; border-radius: 0 8px 8px 0;">
|
|
45
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
|
|
46
|
+
<span style="font-size:18px">${severityIcon(f.severity)}</span>
|
|
47
|
+
<strong style="color:${severityColor(f.severity)}">${f.severity.toUpperCase()}</strong>
|
|
48
|
+
<span style="color:#6b7280; font-size:14px">${SOURCE_LABELS[f.source] || f.source}</span>
|
|
49
|
+
${f.monthlyCost ? `<span style="margin-left:auto; color:${severityColor(f.severity)}; font-weight:bold">$${f.monthlyCost.toFixed(2)}/mo</span>` : ''}
|
|
50
|
+
</div>
|
|
51
|
+
<div style="font-weight:600; color:#111; margin-bottom:4px">${f.message}</div>
|
|
52
|
+
${f.detail ? `<div style="color:#555; font-size:14px; margin-bottom:6px; white-space:pre-line">${f.detail}</div>` : ''}
|
|
53
|
+
${f.fix ? `<div style="color:#16a34a; font-size:14px; margin-top:8px">โ Fix: ${f.fix}</div>` : ''}
|
|
54
|
+
${f.command ? `<div style="background:#0f172a; color:#7dd3fc; font-family:monospace; font-size:12px; padding:8px 12px; border-radius:6px; margin-top:8px">${f.command}</div>` : ''}
|
|
55
|
+
</div>
|
|
56
|
+
`).join('');
|
|
57
|
+
|
|
58
|
+
const sessionRows = (sessions || [])
|
|
59
|
+
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
60
|
+
.slice(0, 20)
|
|
61
|
+
.map(s => {
|
|
62
|
+
const keyDisplay = s.key.length > 18 ? s.key.slice(0, 16) + 'โฆ' : s.key;
|
|
63
|
+
const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
64
|
+
const age = s.ageMs ? relativeAge(s.ageMs) : 'unknown';
|
|
65
|
+
const absDate = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '';
|
|
66
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}/day` : 'โ';
|
|
67
|
+
return `
|
|
68
|
+
<tr style="${s.isOrphaned ? 'background:#fff7ed; color:#111;' : ''}">
|
|
69
|
+
<td style="padding:8px 12px; font-family:monospace; font-size:13px; ${s.isOrphaned ? 'color:#111;' : ''}">${keyDisplay}${flag}</td>
|
|
70
|
+
<td style="padding:8px 12px; ${s.isOrphaned ? 'color:#111;' : ''}">${s.modelLabel || s.model}</td>
|
|
71
|
+
<td style="padding:8px 12px; text-align:right; ${s.isOrphaned ? 'color:#111;' : ''}">${(s.inputTokens + s.outputTokens).toLocaleString()}</td>
|
|
72
|
+
<td style="padding:8px 12px; text-align:right; color:${s.cost > 0.01 ? '#ef4444' : '#22c55e'}">$${s.cost.toFixed(6)}</td>
|
|
73
|
+
<td style="padding:8px 12px; text-align:right; color:#f59e0b">${daily}</td>
|
|
74
|
+
<td style="padding:8px 12px; color:#94a3b8; font-size:13px; ${s.isOrphaned ? 'color:#6b7280;' : ''}" title="${absDate}">${age}</td>
|
|
75
|
+
</tr>
|
|
76
|
+
`}).join('');
|
|
77
|
+
|
|
78
|
+
const html = `<!DOCTYPE html>
|
|
79
|
+
<html lang="en">
|
|
80
|
+
<head>
|
|
81
|
+
<meta charset="UTF-8">
|
|
82
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
83
|
+
<title>Clawculator Report โ ${new Date(analysis.scannedAt).toLocaleString()}</title>
|
|
84
|
+
<style>
|
|
85
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
86
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
|
87
|
+
.header { background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%); padding: 48px 32px; text-align: center; border-bottom: 1px solid #1e40af; }
|
|
88
|
+
.logo { font-size: 42px; font-weight: 900; letter-spacing: -2px; background: linear-gradient(90deg, #38bdf8, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
89
|
+
.tagline { color: #94a3b8; margin-top: 8px; font-size: 16px; }
|
|
90
|
+
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
91
|
+
.cards { display: table; width: 100%; border-spacing: 16px; border-collapse: separate; margin-bottom: 16px; }
|
|
92
|
+
.cards-row { display: table-row; }
|
|
93
|
+
.card { display: table-cell; background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; width: 16.6%; }
|
|
94
|
+
.card-value { font-size: 32px; font-weight: 800; }
|
|
95
|
+
.card-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
|
|
96
|
+
.section { background: #1e293b; border-radius: 12px; padding: 24px; margin-bottom: 24px; border: 1px solid #334155; }
|
|
97
|
+
.section-title { font-size: 18px; font-weight: 700; margin-bottom: 16px; color: #f1f5f9; }
|
|
98
|
+
table { width: 100%; border-collapse: collapse; }
|
|
99
|
+
th { background: #0f172a; padding: 10px 12px; text-align: left; font-size: 13px; color: #cbd5e1; font-weight: 600; }
|
|
100
|
+
td { color: #e2e8f0; }
|
|
101
|
+
tr:nth-child(even) { background: #0f172a33; }
|
|
102
|
+
.footer { text-align: center; color: #475569; font-size: 13px; padding: 32px; }
|
|
103
|
+
.bleed { background: linear-gradient(135deg, #7f1d1d, #991b1b); border-radius: 12px; padding: 20px 24px; margin-bottom: 24px; border: 1px solid #ef4444; }
|
|
104
|
+
.bleed-amount { font-size: 36px; font-weight: 900; color: #fca5a5; }
|
|
105
|
+
</style>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
<div class="header">
|
|
109
|
+
<div class="logo">CLAWCULATOR</div>
|
|
110
|
+
<div class="tagline">Your friendly penny pincher. ยท 100% offline ยท Zero AI ยท Pure deterministic logic</div>
|
|
111
|
+
<div style="color:#64748b; font-size:13px; margin-top:12px">Report generated: ${new Date(analysis.scannedAt).toLocaleString()}</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="container">
|
|
115
|
+
${bleed > 0 ? `
|
|
116
|
+
<div class="bleed">
|
|
117
|
+
<div style="color:#fca5a5; font-size:14px; font-weight:600; margin-bottom:4px">โ ๏ธ ESTIMATED MONTHLY COST EXPOSURE</div>
|
|
118
|
+
<div class="bleed-amount">$${bleed.toFixed(2)}/month</div>
|
|
119
|
+
<div style="color:#fca5a5; font-size:14px; margin-top:4px">Based on current config โ fix the critical issues below to stop the bleed</div>
|
|
120
|
+
</div>` : `
|
|
121
|
+
<div style="background:#14532d; border-radius:12px; padding:20px 24px; margin-bottom:24px; border:1px solid #22c55e">
|
|
122
|
+
<div style="color:#86efac; font-size:18px; font-weight:700">โ
No significant cost bleed detected</div>
|
|
123
|
+
</div>`}
|
|
124
|
+
|
|
125
|
+
<div class="cards"><div class="cards-row">
|
|
126
|
+
<div class="card">
|
|
127
|
+
<div class="card-value" style="color:#ef4444">${summary.critical}</div>
|
|
128
|
+
<div class="card-label">๐ด Critical</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="card">
|
|
131
|
+
<div class="card-value" style="color:#f97316">${summary.high}</div>
|
|
132
|
+
<div class="card-label">๐ High</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="card">
|
|
135
|
+
<div class="card-value" style="color:#eab308">${summary.medium}</div>
|
|
136
|
+
<div class="card-label">๐ก Medium</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="card">
|
|
139
|
+
<div class="card-value" style="color:#38bdf8">${summary.low || 0}</div>
|
|
140
|
+
<div class="card-label">๐ต Low</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="card">
|
|
143
|
+
<div class="card-value" style="color:#22c55e">${summary.info}</div>
|
|
144
|
+
<div class="card-label">โ
OK</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="card">
|
|
147
|
+
<div class="card-value" style="color:#38bdf8">${summary.sessionsAnalyzed}</div>
|
|
148
|
+
<div class="card-label">Sessions Analyzed</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="card">
|
|
151
|
+
<div class="card-value" style="color:#818cf8">${(summary.totalTokensFound || 0).toLocaleString()}</div>
|
|
152
|
+
<div class="card-label">Total Tokens Found</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div></div>
|
|
155
|
+
|
|
156
|
+
<div class="section">
|
|
157
|
+
<div class="section-title">Findings</div>
|
|
158
|
+
<div style="color:#0f172a">
|
|
159
|
+
${findingCards}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
${sessionRows ? `
|
|
164
|
+
<div class="section">
|
|
165
|
+
<div class="section-title">Session Breakdown</div>
|
|
166
|
+
<div style="overflow-x:auto">
|
|
167
|
+
<table>
|
|
168
|
+
<thead><tr><th>Session</th><th>Model</th><th style="text-align:right">Tokens</th><th style="text-align:right">Total Cost</th><th style="text-align:right">$/day</th><th>Last Active</th></tr></thead>
|
|
169
|
+
<tbody>${sessionRows}</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
172
|
+
</div>` : ''}
|
|
173
|
+
|
|
174
|
+
</div>
|
|
175
|
+
<div class="footer">
|
|
176
|
+
Clawculator ยท github.com/echoudhry/clawculator ยท Your friendly penny pincher.
|
|
177
|
+
</div>
|
|
178
|
+
</body>
|
|
179
|
+
</html>`;
|
|
180
|
+
|
|
181
|
+
const finalPath = outPath || path.join(os.tmpdir(), `clawculator-report-${Date.now()}.html`);
|
|
182
|
+
fs.writeFileSync(finalPath, html, 'utf8');
|
|
183
|
+
return finalPath;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { generateHTMLReport };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SEVERITY_ICON = { critical: '๐ด', high: '๐ ', medium: '๐ก', info: 'โ
' };
|
|
4
|
+
const SOURCE_LABELS = {
|
|
5
|
+
heartbeat: '๐ Heartbeat', hooks: '๐ช Hooks', whatsapp: '๐ฑ WhatsApp',
|
|
6
|
+
subagents: '๐ค Subagents', skills: '๐ง Skills', memory: '๐ง Memory',
|
|
7
|
+
primary_model: 'โ๏ธ Primary Model', sessions: '๐ฌ Sessions',
|
|
8
|
+
workspace: '๐ Workspace', config: '๐ Config',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function relativeAge(ageMs) {
|
|
12
|
+
if (!ageMs) return 'unknown';
|
|
13
|
+
const s = Math.floor(ageMs / 1000);
|
|
14
|
+
if (s < 60) return `${s}s ago`;
|
|
15
|
+
const m = Math.floor(s / 60);
|
|
16
|
+
if (m < 60) return `${m}m ago`;
|
|
17
|
+
const h = Math.floor(m / 60);
|
|
18
|
+
if (h < 24) return `${h}h ago`;
|
|
19
|
+
const d = Math.floor(h / 24);
|
|
20
|
+
if (d < 30) return `${d}d ago`;
|
|
21
|
+
const mo = Math.floor(d / 30);
|
|
22
|
+
return `${mo}mo ago`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function absoluteDate(updatedAt) {
|
|
26
|
+
if (!updatedAt) return '';
|
|
27
|
+
return new Date(updatedAt).toLocaleString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function generateMarkdownReport(analysis) {
|
|
31
|
+
const { summary, findings, sessions, primaryModel, scannedAt } = analysis;
|
|
32
|
+
const bleed = summary.estimatedMonthlyBleed;
|
|
33
|
+
const lines = [];
|
|
34
|
+
|
|
35
|
+
lines.push('# Clawculator Report');
|
|
36
|
+
lines.push('> Your friendly penny pincher. ยท 100% offline ยท Zero AI ยท Pure deterministic logic');
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push(`**Scanned:** ${new Date(scannedAt).toLocaleString()}`);
|
|
39
|
+
if (primaryModel) lines.push(`**Primary model:** ${primaryModel}`);
|
|
40
|
+
lines.push('');
|
|
41
|
+
|
|
42
|
+
// Cost alert
|
|
43
|
+
if (bleed > 0) {
|
|
44
|
+
lines.push(`## โ ๏ธ Estimated Monthly Cost Exposure: $${bleed.toFixed(2)}/month`);
|
|
45
|
+
lines.push('');
|
|
46
|
+
} else {
|
|
47
|
+
lines.push('## โ
No significant cost bleed detected');
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Summary table
|
|
52
|
+
lines.push('## Summary');
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push('| Metric | Value |');
|
|
55
|
+
lines.push('|--------|-------|');
|
|
56
|
+
lines.push(`| ๐ด Critical | ${summary.critical} |`);
|
|
57
|
+
lines.push(`| ๐ High | ${summary.high} |`);
|
|
58
|
+
lines.push(`| ๐ก Medium | ${summary.medium} |`);
|
|
59
|
+
lines.push(`| โ
OK | ${summary.info} |`);
|
|
60
|
+
lines.push(`| Sessions analyzed | ${summary.sessionsAnalyzed} |`);
|
|
61
|
+
lines.push(`| Total tokens found | ${(summary.totalTokensFound || 0).toLocaleString()} |`);
|
|
62
|
+
|
|
63
|
+
// Session cost summary
|
|
64
|
+
if (sessions?.length > 0) {
|
|
65
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.cost, 0);
|
|
66
|
+
const dailyRates = sessions.filter(s => s.dailyCost).map(s => s.dailyCost);
|
|
67
|
+
const totalDailyRate = dailyRates.reduce((sum, r) => sum + r, 0);
|
|
68
|
+
if (totalCost > 0) lines.push(`| Total session cost (lifetime) | $${totalCost.toFixed(4)} |`);
|
|
69
|
+
if (totalDailyRate > 0) lines.push(`| Avg daily burn rate | $${totalDailyRate.toFixed(4)}/day (~$${(totalDailyRate * 30).toFixed(2)}/month) |`);
|
|
70
|
+
}
|
|
71
|
+
lines.push('');
|
|
72
|
+
|
|
73
|
+
// Findings by severity
|
|
74
|
+
lines.push('## Findings');
|
|
75
|
+
lines.push('');
|
|
76
|
+
|
|
77
|
+
for (const severity of ['critical', 'high', 'medium', 'info']) {
|
|
78
|
+
const group = findings.filter(f => f.severity === severity);
|
|
79
|
+
if (!group.length) continue;
|
|
80
|
+
|
|
81
|
+
lines.push(`### ${SEVERITY_ICON[severity]} ${severity.toUpperCase()} (${group.length})`);
|
|
82
|
+
lines.push('');
|
|
83
|
+
|
|
84
|
+
for (const f of group) {
|
|
85
|
+
lines.push(`#### ${SOURCE_LABELS[f.source] || f.source}`);
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(`**${f.message}**`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
if (f.detail) lines.push(`${f.detail}`);
|
|
90
|
+
if (f.monthlyCost > 0) lines.push(`**Monthly cost:** $${f.monthlyCost.toFixed(2)}/month`);
|
|
91
|
+
if (f.fix) {
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`**Fix:** ${f.fix}`);
|
|
94
|
+
}
|
|
95
|
+
if (f.command) {
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('```bash');
|
|
98
|
+
lines.push(f.command);
|
|
99
|
+
lines.push('```');
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Session breakdown
|
|
106
|
+
if (sessions?.length > 0) {
|
|
107
|
+
lines.push('## Session Breakdown');
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push('| Session | Model | Tokens | Total Cost | $/day | Last Active |');
|
|
110
|
+
lines.push('|---------|-------|--------|-----------|-------|-------------|');
|
|
111
|
+
|
|
112
|
+
[...sessions]
|
|
113
|
+
.sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens))
|
|
114
|
+
.slice(0, 20)
|
|
115
|
+
.forEach(s => {
|
|
116
|
+
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
117
|
+
const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
118
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
119
|
+
const age = s.ageMs ? `${relativeAge(s.ageMs)} (${absoluteDate(s.updatedAt)})` : 'unknown';
|
|
120
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ';
|
|
121
|
+
lines.push(`| \`${keyDisplay}${flag}\` | ${s.modelLabel || s.model} | ${tok} | $${s.cost.toFixed(6)} | ${daily} | ${age} |`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push('> โ ๏ธ = orphaned session ยท $/day = total cost รท session age');
|
|
126
|
+
lines.push('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Quick wins
|
|
130
|
+
const wins = findings.filter(f => f.fix && f.severity !== 'info');
|
|
131
|
+
if (wins.length > 0) {
|
|
132
|
+
lines.push('## Quick Wins');
|
|
133
|
+
lines.push('');
|
|
134
|
+
wins.slice(0, 5).forEach((f, i) => {
|
|
135
|
+
lines.push(`${i + 1}. **${f.fix}**`);
|
|
136
|
+
if (f.command) lines.push(` \`${f.command}\``);
|
|
137
|
+
});
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push('---');
|
|
142
|
+
lines.push('*Generated by [Clawculator](https://github.com/echoudhry/clawculator) ยท Your friendly penny pincher.*');
|
|
143
|
+
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { generateMarkdownReport };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SEVERITY_CONFIG = {
|
|
4
|
+
critical: { color: '\x1b[31m', icon: '๐ด', label: 'CRITICAL' },
|
|
5
|
+
high: { color: '\x1b[33m', icon: '๐ ', label: 'HIGH' },
|
|
6
|
+
medium: { color: '\x1b[33m', icon: '๐ก', label: 'MEDIUM' },
|
|
7
|
+
low: { color: '\x1b[36m', icon: '๐ต', label: 'LOW' },
|
|
8
|
+
info: { color: '\x1b[32m', icon: 'โ
', label: 'OK' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SOURCE_LABELS = {
|
|
12
|
+
heartbeat: '๐ Heartbeat',
|
|
13
|
+
hooks: '๐ช Hooks',
|
|
14
|
+
whatsapp: '๐ฑ WhatsApp',
|
|
15
|
+
subagents: '๐ค Subagents',
|
|
16
|
+
skills: '๐ง Skills',
|
|
17
|
+
memory: '๐ง Memory',
|
|
18
|
+
primary_model: 'โ๏ธ Primary Model',
|
|
19
|
+
sessions: '๐ฌ Sessions',
|
|
20
|
+
workspace: '๐ Workspace',
|
|
21
|
+
context: '๐ Context Pruning',
|
|
22
|
+
vision: '๐ผ๏ธ Vision Tokens',
|
|
23
|
+
fallbacks: '๐ Model Fallbacks',
|
|
24
|
+
multi_agent: '๐ฅ Multi-Agent',
|
|
25
|
+
cron: 'โฐ Cron Jobs',
|
|
26
|
+
telegram: 'โ๏ธ Telegram',
|
|
27
|
+
discord: '๐ฌ Discord',
|
|
28
|
+
signal: '๐ก Signal',
|
|
29
|
+
config: '๐ Config',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function formatCost(cost) {
|
|
33
|
+
if (!cost) return '';
|
|
34
|
+
if (cost === 0) return '\x1b[32m$0.00/mo\x1b[0m';
|
|
35
|
+
if (cost < 1) return `\x1b[33m$${cost.toFixed(4)}/mo\x1b[0m`;
|
|
36
|
+
return `\x1b[31m$${cost.toFixed(2)}/mo\x1b[0m`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function relativeAge(ageMs) {
|
|
40
|
+
if (!ageMs) return 'unknown';
|
|
41
|
+
const s = Math.floor(ageMs / 1000);
|
|
42
|
+
if (s < 60) return `${s}s ago`;
|
|
43
|
+
const m = Math.floor(s / 60);
|
|
44
|
+
if (m < 60) return `${m}m ago`;
|
|
45
|
+
const h = Math.floor(m / 60);
|
|
46
|
+
if (h < 24) return `${h}h ago`;
|
|
47
|
+
const d = Math.floor(h / 24);
|
|
48
|
+
if (d < 30) return `${d}d ago`;
|
|
49
|
+
return `${Math.floor(d / 30)}mo ago`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function generateTerminalReport(analysis) {
|
|
53
|
+
const { summary, findings, sessions } = analysis;
|
|
54
|
+
const R = '\x1b[0m';
|
|
55
|
+
const B = '\x1b[1m';
|
|
56
|
+
const D = '\x1b[90m';
|
|
57
|
+
const C = '\x1b[36m';
|
|
58
|
+
const RED = '\x1b[31m';
|
|
59
|
+
const GRN = '\x1b[32m';
|
|
60
|
+
|
|
61
|
+
console.log(`${C}โโโ Scan Complete โโโ${R}`);
|
|
62
|
+
console.log(`${D}${new Date(analysis.scannedAt).toLocaleString()}${R}`);
|
|
63
|
+
if (analysis.primaryModel) console.log(`${D}Primary model: ${analysis.primaryModel}${R}`);
|
|
64
|
+
console.log();
|
|
65
|
+
|
|
66
|
+
const bleed = summary.estimatedMonthlyBleed;
|
|
67
|
+
if (bleed > 0) {
|
|
68
|
+
console.log(`${B}${RED}โ ๏ธ Estimated monthly cost exposure: $${bleed.toFixed(2)}/month${R}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
72
|
+
const group = findings.filter(f => f.severity === severity);
|
|
73
|
+
if (!group.length) continue;
|
|
74
|
+
|
|
75
|
+
const cfg = SEVERITY_CONFIG[severity];
|
|
76
|
+
console.log(`${cfg.color}${B}${cfg.icon} ${cfg.label} (${group.length})${R}`);
|
|
77
|
+
console.log(`${C}${'โ'.repeat(60)}${R}`);
|
|
78
|
+
|
|
79
|
+
for (const f of group) {
|
|
80
|
+
console.log(` ${B}${SOURCE_LABELS[f.source] || f.source}${R}`);
|
|
81
|
+
console.log(` ${f.message}`);
|
|
82
|
+
if (f.detail) console.log(` ${D}${f.detail}${R}`);
|
|
83
|
+
if (f.monthlyCost > 0) console.log(` ${D}Cost: ${R}${formatCost(f.monthlyCost)}`);
|
|
84
|
+
if (f.fix) console.log(` ${GRN}โ ${f.fix}${R}`);
|
|
85
|
+
if (f.command) console.log(` ${D} ${f.command}${R}`);
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Session breakdown
|
|
91
|
+
if (sessions?.length > 0) {
|
|
92
|
+
console.log(`${C}โโโ Top Sessions by Token Usage โโโ${R}\n`);
|
|
93
|
+
const sorted = [...sessions].sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens)).slice(0, 8);
|
|
94
|
+
console.log(` ${D}${'Session'.padEnd(16)} ${'Model'.padEnd(20)} ${'Tokens'.padEnd(10)} ${'Total Cost'.padEnd(12)} ${'$/day'.padEnd(10)} Last Active${R}`);
|
|
95
|
+
console.log(` ${D}${'โ'.repeat(95)}${R}`);
|
|
96
|
+
for (const s of sorted) {
|
|
97
|
+
const tok = (s.inputTokens + s.outputTokens).toLocaleString();
|
|
98
|
+
const flag = s.isOrphaned ? ' โ ๏ธ' : '';
|
|
99
|
+
const keyDisplay = s.key.length > 12 ? s.key.slice(0, 8) + 'โฆ' : s.key;
|
|
100
|
+
const age = s.ageMs ? `${relativeAge(s.ageMs)} (${new Date(s.updatedAt).toLocaleDateString()})` : 'unknown';
|
|
101
|
+
const daily = s.dailyCost ? `$${s.dailyCost.toFixed(4)}` : 'โ';
|
|
102
|
+
console.log(` ${(keyDisplay + flag).padEnd(16)} ${(s.modelLabel || s.model || 'unknown').slice(0, 20).padEnd(20)} ${tok.padEnd(10)} $${s.cost.toFixed(6).padEnd(11)} ${daily.padEnd(10)} ${D}${age}${R}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Daily burn rate summary
|
|
106
|
+
const totalDailyRate = sessions.filter(s => s.dailyCost).reduce((sum, s) => sum + s.dailyCost, 0);
|
|
107
|
+
if (totalDailyRate > 0) {
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(` ${D}Combined burn rate: ${R}${RED}$${totalDailyRate.toFixed(4)}/day${R}${D} ยท ~$${(totalDailyRate * 30).toFixed(2)}/month${R}`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Summary
|
|
115
|
+
console.log(`${C}โโโ Summary โโโ${R}`);
|
|
116
|
+
console.log(` ๐ด ${RED}${summary.critical}${R} critical ๐ ${summary.high} high ๐ก ${summary.medium} medium ๐ต ${summary.low||0} low โ
${summary.info} ok`);
|
|
117
|
+
console.log(` Sessions analyzed: ${summary.sessionsAnalyzed} ยท Tokens found: ${(summary.totalTokensFound||0).toLocaleString()}`);
|
|
118
|
+
if (bleed > 0) {
|
|
119
|
+
console.log(` ${RED}${B}Estimated monthly bleed: $${bleed.toFixed(2)}/month${R}`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ${GRN}No significant cost bleed detected โ${R}`);
|
|
122
|
+
}
|
|
123
|
+
console.log();
|
|
124
|
+
|
|
125
|
+
// Quick wins
|
|
126
|
+
const wins = findings.filter(f => f.fix && f.severity !== 'info');
|
|
127
|
+
if (wins.length > 0) {
|
|
128
|
+
console.log(`${C}โโโ Quick Wins โโโ${R}`);
|
|
129
|
+
wins.slice(0, 5).forEach((f, i) => {
|
|
130
|
+
console.log(` ${i + 1}. ${GRN}${f.fix}${R}`);
|
|
131
|
+
if (f.command) console.log(` ${D}${f.command}${R}`);
|
|
132
|
+
});
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { generateTerminalReport };
|
|
138
|
+
|
|
139
|
+
// Source label additions for new sources
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { runAnalysis } = require('./analyzer');
|
|
5
|
+
const { generateTerminalReport } = require('./reporter');
|
|
6
|
+
const { generateMarkdownReport } = require('./mdReport');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const flags = {
|
|
13
|
+
json: args.includes('--json'),
|
|
14
|
+
md: args.includes('--md'),
|
|
15
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
16
|
+
config: args.find(a => a.startsWith('--config='))?.split('=')[1],
|
|
17
|
+
out: args.find(a => a.startsWith('--out='))?.split('=')[1],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const BANNER = `
|
|
21
|
+
\x1b[36m
|
|
22
|
+
โโโโโโโโโโ โโโโโโ โโโ โโโ โโโโโโโ โโโโโโ โโโ โโโโโโโ
|
|
23
|
+
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโ
|
|
24
|
+
โโโ โโโ โโโโโโโโโโโ โโ โโโโโโ โโโโโโโโโโโ โโโ
|
|
25
|
+
โโโ โโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โโโ
|
|
26
|
+
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
|
|
27
|
+
โโโโโโโโโโโโโโโโโโ โโโ โโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโ โโโโโโโ
|
|
28
|
+
\x1b[0m
|
|
29
|
+
\x1b[33mYour friendly penny pincher.\x1b[0m
|
|
30
|
+
\x1b[90m100% offline ยท Zero AI ยท Pure deterministic logic ยท Your data never leaves your machine\x1b[0m
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
Usage: node run.js [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
(no flags) Full terminal analysis
|
|
38
|
+
--md Save markdown report to ./clawculator-report.md
|
|
39
|
+
--json Output raw JSON to stdout
|
|
40
|
+
--out=PATH Custom output path for --md
|
|
41
|
+
--config=PATH Path to openclaw.json (auto-detected by default)
|
|
42
|
+
--help, -h Show this help
|
|
43
|
+
|
|
44
|
+
Files read (read-only):
|
|
45
|
+
~/.openclaw/openclaw.json
|
|
46
|
+
~/.openclaw/agents/main/sessions/sessions.json
|
|
47
|
+
~/clawd/ (file count only, no contents)
|
|
48
|
+
/tmp/openclaw (if present)
|
|
49
|
+
|
|
50
|
+
Files written (only when --md is used):
|
|
51
|
+
./clawculator-report.md (or --out path)
|
|
52
|
+
|
|
53
|
+
No network requests are made. No data leaves your machine.
|
|
54
|
+
Session keys are truncated in all output to protect sensitive identifiers.
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
if (flags.help) {
|
|
59
|
+
console.log(BANNER);
|
|
60
|
+
console.log(HELP);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(BANNER);
|
|
65
|
+
console.log('\x1b[90mScanning your setup...\x1b[0m\n');
|
|
66
|
+
|
|
67
|
+
const configPath = flags.config || path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
68
|
+
const sessionsPath = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions', 'sessions.json');
|
|
69
|
+
const logsDir = '/tmp/openclaw';
|
|
70
|
+
|
|
71
|
+
let analysis;
|
|
72
|
+
try {
|
|
73
|
+
analysis = await runAnalysis({ configPath, sessionsPath, logsDir });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('\x1b[31mError:\x1b[0m', err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (flags.json) {
|
|
80
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (flags.md) {
|
|
85
|
+
const outPath = flags.out || path.join(process.cwd(), 'clawculator-report.md');
|
|
86
|
+
fs.writeFileSync(outPath, generateMarkdownReport(analysis), 'utf8');
|
|
87
|
+
console.log(`\x1b[32mโ Markdown report saved:\x1b[0m ${outPath}`);
|
|
88
|
+
generateTerminalReport(analysis);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
generateTerminalReport(analysis);
|
|
93
|
+
console.log('\x1b[90mโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\x1b[0m');
|
|
94
|
+
console.log('\x1b[36mClawculator\x1b[0m ยท github.com/echoudhry/clawculator ยท Your friendly penny pincher.');
|
|
95
|
+
console.log('\x1b[90mTip: --md saves a report your AI agent can read directly\x1b[0m');
|
|
96
|
+
console.log('\x1b[90mโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\x1b[0m\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch(err => {
|
|
100
|
+
console.error('\x1b[31mFatal:\x1b[0m', err.message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
package/logo.png
DELETED
|
Binary file
|