clawculator 2.8.4 → 2.9.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.
@@ -172,10 +172,10 @@ async function main() {
172
172
  const { generateSnapshotCard } = require('../src/snapshotCard');
173
173
  const result = generateSnapshotCard(analysis, outDir);
174
174
  const { exec } = require('child_process');
175
- exec(`open "${result.htmlPath}" 2>/dev/null || xdg-open "${result.htmlPath}" 2>/dev/null`, () => {
175
+ const openFile = result.pngPath || result.htmlPath;
176
+ exec(`open "${openFile}" 2>/dev/null || xdg-open "${openFile}" 2>/dev/null`, () => {
176
177
  process.exit(0);
177
178
  });
178
- // Don't exit immediately — give exec time to open browser
179
179
  setTimeout(() => process.exit(0), 2000);
180
180
  return;
181
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawculator",
3
- "version": "2.8.4",
3
+ "version": "2.9.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": {
@@ -30,8 +30,8 @@
30
30
  "engines": {
31
31
  "node": ">=18.0.0"
32
32
  },
33
- "dependencies": {},
34
33
  "optionalDependencies": {
35
- "better-sqlite3": "^11.0.0"
34
+ "better-sqlite3": "^11.0.0",
35
+ "canvas": "^3.2.1"
36
36
  }
37
37
  }
@@ -349,6 +349,149 @@ function generateSnapshotCard(analysis, outputDir) {
349
349
  const htmlPath = path.join(outputDir, 'clawculator-snapshot.html');
350
350
  fs.writeFileSync(htmlPath, html, 'utf8');
351
351
 
352
+ // ── Generate PNG card ──────────────────────────────────
353
+ let pngPath = null;
354
+ try {
355
+ const { createCanvas } = require('canvas');
356
+ const W = 1080, H = 1080;
357
+ const canvas = createCanvas(W, H);
358
+ const ctx = canvas.getContext('2d');
359
+
360
+ // Background
361
+ ctx.fillStyle = '#05080f';
362
+ ctx.fillRect(0, 0, W, H);
363
+
364
+ // Grid lines
365
+ ctx.strokeStyle = 'rgba(34,211,238,0.03)';
366
+ ctx.lineWidth = 1;
367
+ for (let x = 0; x < W; x += 54) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
368
+ for (let y = 0; y < H; y += 54) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
369
+
370
+ // Border
371
+ ctx.strokeStyle = '#1a2744';
372
+ ctx.lineWidth = 2;
373
+ ctx.roundRect(24, 24, W - 48, H - 48, 24);
374
+ ctx.stroke();
375
+
376
+ // ── Header
377
+ ctx.font = 'bold 36px sans-serif';
378
+ ctx.fillStyle = '#22d3ee';
379
+ ctx.textAlign = 'center';
380
+ ctx.fillText('CLAWCULATOR', W / 2 + 20, 100);
381
+
382
+ // Lobster emoji substitute
383
+ ctx.font = '44px sans-serif';
384
+ ctx.fillText('🦞', W / 2 - 160, 102);
385
+
386
+ // ── Grade circle
387
+ const cx = W / 2, cy = 280, r = 100;
388
+ // Outer glow
389
+ ctx.beginPath(); ctx.arc(cx, cy, r + 8, 0, Math.PI * 2);
390
+ ctx.fillStyle = gradeGlow; ctx.fill();
391
+ // Ring
392
+ ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2);
393
+ ctx.strokeStyle = gradeColor + '66'; ctx.lineWidth = 4; ctx.stroke();
394
+ ctx.beginPath(); ctx.arc(cx, cy, r - 3, 0, Math.PI * 2);
395
+ ctx.strokeStyle = gradeColor + 'aa'; ctx.lineWidth = 2; ctx.stroke();
396
+ // Grade letter
397
+ ctx.font = 'bold 72px sans-serif';
398
+ ctx.fillStyle = gradeColor;
399
+ ctx.textAlign = 'center';
400
+ ctx.textBaseline = 'middle';
401
+ ctx.fillText(grade, cx, cy);
402
+ ctx.textBaseline = 'alphabetic';
403
+ // Label
404
+ ctx.font = '16px sans-serif';
405
+ ctx.fillStyle = '#64748b';
406
+ ctx.fillText('COST HEALTH GRADE', cx, cy + r + 30);
407
+
408
+ // ── Cost range
409
+ ctx.font = 'bold 36px sans-serif';
410
+ ctx.fillStyle = '#e2e8f0';
411
+ ctx.fillText(costEmoji + ' ' + costRange, cx, 460);
412
+
413
+ // ── Divider
414
+ ctx.beginPath();
415
+ ctx.moveTo(W / 2 - 60, 500);
416
+ ctx.lineTo(W / 2 + 60, 500);
417
+ ctx.strokeStyle = '#1e3a5f'; ctx.lineWidth = 1; ctx.stroke();
418
+
419
+ // ── Pills
420
+ ctx.font = '20px sans-serif';
421
+ ctx.fillStyle = '#94a3b8';
422
+ const pillLines = [];
423
+ let currentLine = '';
424
+ for (const p of pills) {
425
+ const item = p.icon + ' ' + p.label;
426
+ const test = currentLine ? currentLine + ' ' + item : item;
427
+ if (test.length > 45 && currentLine) {
428
+ pillLines.push(currentLine);
429
+ currentLine = item;
430
+ } else {
431
+ currentLine = test;
432
+ }
433
+ }
434
+ if (currentLine) pillLines.push(currentLine);
435
+
436
+ let pillY = 545;
437
+ for (const line of pillLines) {
438
+ ctx.fillText(line, cx, pillY);
439
+ pillY += 32;
440
+ }
441
+
442
+ // ── Findings
443
+ const findY = pillY + 30;
444
+ if (findingSummary.length > 0) {
445
+ ctx.font = 'bold 22px sans-serif';
446
+ findingSummary.forEach((f, i) => {
447
+ const c = f.startsWith('🔴') ? '#ef4444' : f.startsWith('🟠') ? '#f97316' : '#eab308';
448
+ ctx.fillStyle = c;
449
+ ctx.fillText(f, cx, findY + i * 32);
450
+ });
451
+ } else {
452
+ ctx.font = 'bold 22px sans-serif';
453
+ ctx.fillStyle = '#22c55e';
454
+ ctx.fillText('✅ No issues found', cx, findY);
455
+ }
456
+
457
+ // ── Divider 2
458
+ const divY2 = findY + (findingSummary.length > 0 ? findingSummary.length * 32 + 20 : 50);
459
+ ctx.beginPath();
460
+ ctx.moveTo(80, divY2);
461
+ ctx.lineTo(W - 80, divY2);
462
+ ctx.strokeStyle = '#1a2744'; ctx.lineWidth = 1; ctx.stroke();
463
+
464
+ // ── CTA
465
+ ctx.font = '18px sans-serif';
466
+ ctx.fillStyle = '#64748b';
467
+ ctx.fillText('Get your OpenClaw cost grade', cx, divY2 + 40);
468
+
469
+ // Command box
470
+ const cmdY = divY2 + 70;
471
+ const cmdW = 380, cmdH = 44;
472
+ ctx.fillStyle = '#0b1120';
473
+ ctx.strokeStyle = '#1a2744';
474
+ ctx.lineWidth = 1;
475
+ ctx.roundRect(cx - cmdW / 2, cmdY - cmdH / 2, cmdW, cmdH, 8);
476
+ ctx.fill(); ctx.stroke();
477
+
478
+ ctx.font = 'bold 20px monospace';
479
+ ctx.fillStyle = '#22d3ee';
480
+ ctx.fillText('npx clawculator --snapshot', cx, cmdY + 6);
481
+
482
+ // Subtitle
483
+ ctx.font = '13px sans-serif';
484
+ ctx.fillStyle = '#334155';
485
+ ctx.fillText('100% offline · your data never leaves your machine', cx, cmdY + 38);
486
+
487
+ // Write PNG
488
+ pngPath = path.join(outputDir, 'clawculator-snapshot.png');
489
+ fs.writeFileSync(pngPath, canvas.toBuffer('image/png'));
490
+ } catch (e) {
491
+ // canvas not installed — PNG generation skipped, HTML still works
492
+ pngPath = null;
493
+ }
494
+
352
495
  // ── Terminal card (screenshot-ready) ───────────────────
353
496
  const C = '\x1b[36m'; // cyan
354
497
  const G = gradeColor === '#22c55e' ? '\x1b[32m' : gradeColor === '#f59e0b' ? '\x1b[33m' : gradeColor === '#f97316' ? '\x1b[33m' : '\x1b[31m';
@@ -392,11 +535,15 @@ function generateSnapshotCard(analysis, outputDir) {
392
535
 
393
536
  console.log(lines.join('\n'));
394
537
 
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`);
538
+ if (pngPath) {
539
+ console.log(` \x1b[32m✓ PNG card saved:\x1b[0m ${pngPath}`);
540
+ console.log(` \x1b[90mDrag into Twitter, Discord, or any social feed!\x1b[0m\n`);
541
+ } else {
542
+ console.log(` \x1b[90mHTML card saved: ${htmlPath}\x1b[0m`);
543
+ console.log(` \x1b[90mTip: npm install canvas — to auto-generate a PNG for sharing\x1b[0m\n`);
544
+ }
398
545
 
399
- return { htmlPath, grade, costRange };
546
+ return { htmlPath, pngPath, grade, costRange };
400
547
  }
401
548
 
402
549
  module.exports = { generateSnapshotCard };
@@ -349,6 +349,149 @@ function generateSnapshotCard(analysis, outputDir) {
349
349
  const htmlPath = path.join(outputDir, 'clawculator-snapshot.html');
350
350
  fs.writeFileSync(htmlPath, html, 'utf8');
351
351
 
352
+ // ── Generate PNG card ──────────────────────────────────
353
+ let pngPath = null;
354
+ try {
355
+ const { createCanvas } = require('canvas');
356
+ const W = 1080, H = 1080;
357
+ const canvas = createCanvas(W, H);
358
+ const ctx = canvas.getContext('2d');
359
+
360
+ // Background
361
+ ctx.fillStyle = '#05080f';
362
+ ctx.fillRect(0, 0, W, H);
363
+
364
+ // Grid lines
365
+ ctx.strokeStyle = 'rgba(34,211,238,0.03)';
366
+ ctx.lineWidth = 1;
367
+ for (let x = 0; x < W; x += 54) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
368
+ for (let y = 0; y < H; y += 54) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
369
+
370
+ // Border
371
+ ctx.strokeStyle = '#1a2744';
372
+ ctx.lineWidth = 2;
373
+ ctx.roundRect(24, 24, W - 48, H - 48, 24);
374
+ ctx.stroke();
375
+
376
+ // ── Header
377
+ ctx.font = 'bold 36px sans-serif';
378
+ ctx.fillStyle = '#22d3ee';
379
+ ctx.textAlign = 'center';
380
+ ctx.fillText('CLAWCULATOR', W / 2 + 20, 100);
381
+
382
+ // Lobster emoji substitute
383
+ ctx.font = '44px sans-serif';
384
+ ctx.fillText('🦞', W / 2 - 160, 102);
385
+
386
+ // ── Grade circle
387
+ const cx = W / 2, cy = 280, r = 100;
388
+ // Outer glow
389
+ ctx.beginPath(); ctx.arc(cx, cy, r + 8, 0, Math.PI * 2);
390
+ ctx.fillStyle = gradeGlow; ctx.fill();
391
+ // Ring
392
+ ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2);
393
+ ctx.strokeStyle = gradeColor + '66'; ctx.lineWidth = 4; ctx.stroke();
394
+ ctx.beginPath(); ctx.arc(cx, cy, r - 3, 0, Math.PI * 2);
395
+ ctx.strokeStyle = gradeColor + 'aa'; ctx.lineWidth = 2; ctx.stroke();
396
+ // Grade letter
397
+ ctx.font = 'bold 72px sans-serif';
398
+ ctx.fillStyle = gradeColor;
399
+ ctx.textAlign = 'center';
400
+ ctx.textBaseline = 'middle';
401
+ ctx.fillText(grade, cx, cy);
402
+ ctx.textBaseline = 'alphabetic';
403
+ // Label
404
+ ctx.font = '16px sans-serif';
405
+ ctx.fillStyle = '#64748b';
406
+ ctx.fillText('COST HEALTH GRADE', cx, cy + r + 30);
407
+
408
+ // ── Cost range
409
+ ctx.font = 'bold 36px sans-serif';
410
+ ctx.fillStyle = '#e2e8f0';
411
+ ctx.fillText(costEmoji + ' ' + costRange, cx, 460);
412
+
413
+ // ── Divider
414
+ ctx.beginPath();
415
+ ctx.moveTo(W / 2 - 60, 500);
416
+ ctx.lineTo(W / 2 + 60, 500);
417
+ ctx.strokeStyle = '#1e3a5f'; ctx.lineWidth = 1; ctx.stroke();
418
+
419
+ // ── Pills
420
+ ctx.font = '20px sans-serif';
421
+ ctx.fillStyle = '#94a3b8';
422
+ const pillLines = [];
423
+ let currentLine = '';
424
+ for (const p of pills) {
425
+ const item = p.icon + ' ' + p.label;
426
+ const test = currentLine ? currentLine + ' ' + item : item;
427
+ if (test.length > 45 && currentLine) {
428
+ pillLines.push(currentLine);
429
+ currentLine = item;
430
+ } else {
431
+ currentLine = test;
432
+ }
433
+ }
434
+ if (currentLine) pillLines.push(currentLine);
435
+
436
+ let pillY = 545;
437
+ for (const line of pillLines) {
438
+ ctx.fillText(line, cx, pillY);
439
+ pillY += 32;
440
+ }
441
+
442
+ // ── Findings
443
+ const findY = pillY + 30;
444
+ if (findingSummary.length > 0) {
445
+ ctx.font = 'bold 22px sans-serif';
446
+ findingSummary.forEach((f, i) => {
447
+ const c = f.startsWith('🔴') ? '#ef4444' : f.startsWith('🟠') ? '#f97316' : '#eab308';
448
+ ctx.fillStyle = c;
449
+ ctx.fillText(f, cx, findY + i * 32);
450
+ });
451
+ } else {
452
+ ctx.font = 'bold 22px sans-serif';
453
+ ctx.fillStyle = '#22c55e';
454
+ ctx.fillText('✅ No issues found', cx, findY);
455
+ }
456
+
457
+ // ── Divider 2
458
+ const divY2 = findY + (findingSummary.length > 0 ? findingSummary.length * 32 + 20 : 50);
459
+ ctx.beginPath();
460
+ ctx.moveTo(80, divY2);
461
+ ctx.lineTo(W - 80, divY2);
462
+ ctx.strokeStyle = '#1a2744'; ctx.lineWidth = 1; ctx.stroke();
463
+
464
+ // ── CTA
465
+ ctx.font = '18px sans-serif';
466
+ ctx.fillStyle = '#64748b';
467
+ ctx.fillText('Get your OpenClaw cost grade', cx, divY2 + 40);
468
+
469
+ // Command box
470
+ const cmdY = divY2 + 70;
471
+ const cmdW = 380, cmdH = 44;
472
+ ctx.fillStyle = '#0b1120';
473
+ ctx.strokeStyle = '#1a2744';
474
+ ctx.lineWidth = 1;
475
+ ctx.roundRect(cx - cmdW / 2, cmdY - cmdH / 2, cmdW, cmdH, 8);
476
+ ctx.fill(); ctx.stroke();
477
+
478
+ ctx.font = 'bold 20px monospace';
479
+ ctx.fillStyle = '#22d3ee';
480
+ ctx.fillText('npx clawculator --snapshot', cx, cmdY + 6);
481
+
482
+ // Subtitle
483
+ ctx.font = '13px sans-serif';
484
+ ctx.fillStyle = '#334155';
485
+ ctx.fillText('100% offline · your data never leaves your machine', cx, cmdY + 38);
486
+
487
+ // Write PNG
488
+ pngPath = path.join(outputDir, 'clawculator-snapshot.png');
489
+ fs.writeFileSync(pngPath, canvas.toBuffer('image/png'));
490
+ } catch (e) {
491
+ // canvas not installed — PNG generation skipped, HTML still works
492
+ pngPath = null;
493
+ }
494
+
352
495
  // ── Terminal card (screenshot-ready) ───────────────────
353
496
  const C = '\x1b[36m'; // cyan
354
497
  const G = gradeColor === '#22c55e' ? '\x1b[32m' : gradeColor === '#f59e0b' ? '\x1b[33m' : gradeColor === '#f97316' ? '\x1b[33m' : '\x1b[31m';
@@ -392,11 +535,15 @@ function generateSnapshotCard(analysis, outputDir) {
392
535
 
393
536
  console.log(lines.join('\n'));
394
537
 
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`);
538
+ if (pngPath) {
539
+ console.log(` \x1b[32m✓ PNG card saved:\x1b[0m ${pngPath}`);
540
+ console.log(` \x1b[90mDrag into Twitter, Discord, or any social feed!\x1b[0m\n`);
541
+ } else {
542
+ console.log(` \x1b[90mHTML card saved: ${htmlPath}\x1b[0m`);
543
+ console.log(` \x1b[90mTip: npm install canvas — to auto-generate a PNG for sharing\x1b[0m\n`);
544
+ }
398
545
 
399
- return { htmlPath, grade, costRange };
546
+ return { htmlPath, pngPath, grade, costRange };
400
547
  }
401
548
 
402
549
  module.exports = { generateSnapshotCard };