dankgrinder 8.28.0 → 8.32.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.
Files changed (3) hide show
  1. package/lib/grinder.js +61 -42
  2. package/lib/ui.js +144 -0
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -8,6 +8,7 @@ const {
8
8
  AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
9
9
  } = require('./structures');
10
10
  const PKG_VERSION = require('../package.json').version;
11
+ const ui = require('./ui');
11
12
 
12
13
  // Global shutdown flag
13
14
  let shutdownCalled = false;
@@ -88,14 +89,13 @@ async function sendWebhook(title, description, color = 0x5865f2) {
88
89
  }
89
90
 
90
91
  // ── Terminal Colors & ANSI ───────────────────────────────────
91
- // All colors stripped — plain text output only
92
92
  const c = {
93
- reset: '', bold: '', dim: '', italic: '',
94
- green: '', red: '', yellow: '', cyan: '',
95
- magenta: '', white: '', blue: '',
96
- bgGreen: '', bgRed: '', bgYellow: '', bgCyan: '',
97
- bgMagenta: '', bgBlue: '', bgWhite: '',
98
- // Cursor control (kept for functional use)
93
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
94
+ green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m',
95
+ magenta: '\x1b[35m', white: '\x1b[37m', blue: '\x1b[34m',
96
+ bgGreen: '\x1b[42m', bgRed: '\x1b[41m', bgYellow: '\x1b[43m', bgCyan: '\x1b[46m',
97
+ bgMagenta: '\x1b[45m', bgBlue: '\x1b[44m', bgWhite: '\x1b[47m',
98
+ // Cursor control
99
99
  clearLine: '\x1b[2K',
100
100
  cursorUp: (n) => `\x1b[${n}A`,
101
101
  cursorTo: (col) => `\x1b[${col}G`,
@@ -230,21 +230,34 @@ async function filterClaimableAccounts(accounts) {
230
230
  return claimable;
231
231
  }
232
232
 
233
- // ── Truecolor gradient helpers (disabled — plain text only) ──
234
- function rgb(r, g, b) { return ''; }
235
- function bgRgb(r, g, b) { return ''; }
233
+ // ── Truecolor gradient helpers ─────────────────────────────────
234
+ function rgb(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
235
+ function bgRgb(r, g, b) { return `\x1b[48;2;${r};${g};${b}m`; }
236
236
  function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
237
237
 
238
238
  function gradientLine(text, from, to) {
239
- return text;
239
+ // from/to are [r,g,b] arrays
240
+ const fr = Array.isArray(from) ? from[0] : 128;
241
+ const fg = Array.isArray(from) ? from[1] : 128;
242
+ const fb = Array.isArray(from) ? from[2] : 128;
243
+ const tr = Array.isArray(to) ? to[0] : 255;
244
+ const tg = Array.isArray(to) ? to[1] : 255;
245
+ const tb = Array.isArray(to) ? to[2] : 255;
246
+ let out = '';
247
+ for (let i = 0; i < text.length; i++) {
248
+ const t = text.length <= 1 ? 0 : i / (text.length - 1);
249
+ out += `\x1b[38;2;${lerp(fr, tr, t)};${lerp(fg, tg, t)};${lerp(fb, tb, t)}m${text[i]}`;
250
+ }
251
+ return out + c.reset;
240
252
  }
241
253
 
242
254
  function gradientText(text, from, to) {
243
- return text;
255
+ return gradientLine(text, from, to);
244
256
  }
245
257
 
246
258
  function colorBanner() {
247
- return `DANKGRINDER v${PKG_VERSION}`;
259
+ const title = `DANKGRINDER v${PKG_VERSION}`;
260
+ return gradientLine(title, [77, 142, 255], [255, 92, 147]);
248
261
  }
249
262
 
250
263
  // ── Simple Logging ─────────────────────────────────────────────
@@ -2647,7 +2660,8 @@ captchaDetector.build();
2647
2660
  // ══════════════════════════════════════════════════════════════
2648
2661
 
2649
2662
  async function start(apiKey, apiUrl, opts = {}) {
2650
- console.log('DANKGRINDER starting...');
2663
+ ui.init({ workers, getUptime: formatUptime, isShuttingDown: () => shutdownCalled });
2664
+ ui.drawBanner(PKG_VERSION);
2651
2665
  CLOUD_ADMIN_KEY = process.env.CLOUD_ADMIN_KEY || '';
2652
2666
  API_KEY = apiKey;
2653
2667
  API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
@@ -2870,7 +2884,8 @@ async function start(apiKey, apiUrl, opts = {}) {
2870
2884
  for (const w of activeWorkers) {
2871
2885
  if (!shutdownCalled) w.grindLoop();
2872
2886
  }
2873
- console.log(`All grind loops started. v${PKG_VERSION} | Ctrl+C to stop`);
2887
+ ui.start();
2888
+ console.log(`${c.dim}All grind loops started. | Ctrl+C to stop${c.reset}`);
2874
2889
 
2875
2890
  // Cluster heartbeat — lets other nodes see this node is alive
2876
2891
  if (CLUSTER_ENABLED) {
@@ -2931,64 +2946,68 @@ async function start(apiKey, apiUrl, opts = {}) {
2931
2946
  } catch {}
2932
2947
  }, 10_000);
2933
2948
 
2934
- let sigintHandled = false;
2935
- process.on('SIGINT', async () => {
2936
- if (sigintHandled) return;
2937
- sigintHandled = true;
2949
+ let shutdownInProgress = false;
2950
+
2951
+ async function gracefulShutdown(signal) {
2952
+ if (shutdownInProgress) return;
2953
+ shutdownInProgress = true;
2938
2954
  shutdownCalled = true;
2939
2955
  setDashboardActive(false);
2940
- process.stdout.write(c.show);
2956
+ ui.stop();
2957
+ process.stdout.write(c.show + '\n');
2941
2958
 
2942
2959
  console.log('');
2943
- console.log('Session Summary');
2960
+ console.log(`${c.yellow}[${signal}] Shutting down...${c.reset}`);
2944
2961
 
2945
- // Collect stats from all workers (including rotated-out ones)
2962
+ // Collect stats from all workers
2946
2963
  let finalCoins = 0;
2947
2964
  let finalCmds = 0;
2948
2965
  for (const wk of workers) {
2949
2966
  const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
2950
- console.log(` ${(wk.username || '?').padEnd(18)} +${wk.stats.coins.toLocaleString().padStart(8)} coins ${wk.stats.commands} cmds ${rate}% ok`);
2967
+ console.log(` ${c.dim}${wk.username || '?'}${c.reset} ${c.green}+⏣${wk.stats.coins.toLocaleString()}${c.reset} ${wk.stats.commands}cmds ${rate}%ok`);
2951
2968
  finalCoins += wk.stats.coins || 0;
2952
2969
  finalCmds += wk.stats.commands || 0;
2953
2970
  }
2954
2971
 
2955
2972
  const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
2956
- const avgEarn = globalEarningsEMA.get();
2957
2973
  const cpm = globalCmdRate.getRate().toFixed(1);
2958
- console.log(`Total: +${finalCoins.toLocaleString()} coins in ${formatUptime()} | ${finalCmds} cmds | ~${cpm} cmd/m | ${memFinal}MB`);
2959
- console.log('');
2974
+ console.log(`${c.bold}Total:${c.reset} +⏣${finalCoins.toLocaleString()} in ${formatUptime()} | ${finalCmds}cmds | ~${cpm}cmd/m | ${memFinal}MB`);
2960
2975
 
2961
- // Release all cluster claims before stopping workers
2962
- const releasePromises = workers.map(wk => releaseClaim(wk.account.id).catch(() => {}));
2963
- await Promise.all(releasePromises).catch(() => {});
2964
-
2965
- for (const wk of workers) wk.stop();
2976
+ // Stop workers max 1s per worker so one hung client doesn't block shutdown
2977
+ await Promise.all(workers.map(wk => Promise.race([
2978
+ new Promise(resolve => { wk.stop(); resolve(true); }),
2979
+ new Promise(resolve => setTimeout(() => resolve(false), 1000)),
2980
+ ])));
2966
2981
  workerMap.clear();
2967
2982
 
2968
- // Remove this node's heartbeat from Redis
2983
+ // Release cluster claims
2984
+ await Promise.allSettled(workers.map(wk => releaseClaim(wk.account.id)));
2985
+
2986
+ // Remove heartbeat
2969
2987
  if (redis && CLUSTER_ENABLED) {
2970
- try { await redis.del(`${CLUSTER_PREFIX}node:${NODE_ID}`); } catch {}
2988
+ await Promise.allSettled([redis.del(`${CLUSTER_PREFIX}node:${NODE_ID}`)]);
2971
2989
  }
2972
2990
 
2973
2991
  const totalRecoveries = workers.reduce((sum, wk) => sum + (wk._totalRecoveries || 0), 0);
2974
2992
  const totalDisconnects = workers.reduce((sum, wk) => sum + (wk._disconnectCount || 0), 0);
2975
2993
  const totalRateLimits = workers.reduce((sum, wk) => sum + (wk._rateLimitHits || 0), 0);
2976
2994
 
2995
+ if (totalRecoveries > 0 || totalDisconnects > 0) {
2996
+ console.log(` ${c.dim}${totalRecoveries} recoveries, ${totalDisconnects} disconnects, ${totalRateLimits} rate-limits${c.reset}`);
2997
+ }
2998
+
2977
2999
  const webhookMsg = `+⏣ ${finalCoins.toLocaleString()} | ${finalCmds} cmds | ${formatUptime()}` +
2978
3000
  (totalRecoveries > 0 ? ` | ${totalRecoveries} auto-recoveries` : '') +
2979
3001
  (CLUSTER_ENABLED ? ` | node: ${NODE_ID.substring(0, 12)}` : '');
2980
3002
  sendWebhook('Session Ended', webhookMsg, 0x8b5cf6);
2981
3003
 
2982
- if (totalRecoveries > 0 || totalDisconnects > 0) {
2983
- console.log(` ${c.dim}Recovery stats: ${totalRecoveries} auto-recoveries, ${totalDisconnects} disconnects, ${totalRateLimits} rate limits${c.reset}`);
2984
- }
2985
- if (CLUSTER_ENABLED) {
2986
- console.log(` ${c.dim}Cluster node: ${NODE_ID} — claims released${c.reset}`);
2987
- }
3004
+ if (redis) { redis.disconnect().catch(() => {}); }
3005
+ console.log(`${c.green}Goodbye!${c.reset}\n`);
3006
+ process.exit(0);
3007
+ }
2988
3008
 
2989
- if (redis) { try { redis.disconnect(); } catch {} }
2990
- setTimeout(() => process.exit(0), 1500);
2991
- });
3009
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3010
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
2992
3011
  }
2993
3012
 
2994
3013
  // ══════════════════════════════════════════════════════════════
package/lib/ui.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * CLI Live Dashboard
3
+ * Renders a terminal table that updates in place every 10s.
4
+ * Shows per-account status, earned coins, commands, and session uptime.
5
+ */
6
+
7
+ let _workers = [];
8
+ let _getUptime = () => '0s';
9
+ let _isShuttingDown = () => false;
10
+
11
+ // ANSI helpers
12
+ const c = {
13
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
14
+ green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m',
15
+ cyan: '\x1b[36m', blue: '\x1b[34m',
16
+ };
17
+ const DIM = c.dim;
18
+
19
+ function rgb(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
20
+ function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
21
+ function gradientLine(text, from, to) {
22
+ const fr = Array.isArray(from) ? from[0] : 128;
23
+ const fg = Array.isArray(from) ? from[1] : 128;
24
+ const fb = Array.isArray(from) ? from[2] : 128;
25
+ const tr = Array.isArray(to) ? to[0] : 255;
26
+ const tg = Array.isArray(to) ? to[1] : 255;
27
+ const tb = Array.isArray(to) ? to[2] : 255;
28
+ let out = '';
29
+ for (let i = 0; i < text.length; i++) {
30
+ const t = text.length <= 1 ? 0 : i / (text.length - 1);
31
+ out += `\x1b[38;2;${lerp(fr, tr, t)};${lerp(fg, tg, t)};${lerp(fb, tb, t)}m${text[i]}`;
32
+ }
33
+ return out + c.reset;
34
+ }
35
+
36
+ let _lineCount = 0;
37
+ let _interval = null;
38
+
39
+ function _statusIcon(w) {
40
+ if (!w.running || !w.channel) return `${c.red}✗${c.reset}`;
41
+ if (w.paused || w.dashboardPaused) return `${c.yellow}⏸${c.reset}`;
42
+ if (w.busy || w._invRunning || w._sellRunning) return `${c.cyan}⚙${c.reset}`;
43
+ if (Date.now() < w.globalCooldownUntil) return `${c.blue}⏳${c.reset}`;
44
+ return `${c.green}●${c.reset}`;
45
+ }
46
+
47
+ function _render() {
48
+ const workers = _workers;
49
+ const W = Math.min(process.stdout.columns || 120, 120);
50
+ const lines = [];
51
+
52
+ lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
53
+ lines.push(
54
+ ` ${c.bold}#${c.reset} ${c.bold}Account${c.reset}${' '.repeat(Math.max(1, 20 - 7))}` +
55
+ `${c.bold}Status${c.reset}${' '.repeat(Math.max(1, 14 - 6))}` +
56
+ `${c.bold}Earned${c.reset}${' '.repeat(Math.max(1, 12 - 5))}` +
57
+ `${c.bold}Cmds${c.reset}${' '.repeat(Math.max(1, 7 - 4))}` +
58
+ `${c.bold}OK%${c.reset}`
59
+ );
60
+ lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
61
+
62
+ for (let i = 0; i < workers.length; i++) {
63
+ const w = workers[i];
64
+ const name = (w.username || '?').substring(0, 20);
65
+ const status = (w.lastStatus || 'idle').substring(0, 14).padEnd(14);
66
+ const earned = w.stats.coins > 0 ? `+⏣${w.stats.coins.toLocaleString()}` : DIM + '—' + c.reset;
67
+ const cmds = String(w.stats.commands || 0);
68
+ const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
69
+
70
+ lines.push(
71
+ ` ${DIM}${String(i + 1).padStart(2)}${c.reset} ${name.padEnd(20)} ${status}` +
72
+ `${String(earned).padStart(12)} ${cmds.padStart(6)} ${String(rate).padStart(3)}%`
73
+ );
74
+ }
75
+
76
+ lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
77
+
78
+ // Totals
79
+ let totalCoins = 0, totalCmds = 0;
80
+ for (const w of workers) {
81
+ totalCoins += w.stats.coins || 0;
82
+ totalCmds += w.stats.commands || 0;
83
+ }
84
+ const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
85
+ lines.push(
86
+ ` ${c.bold}Total${c.reset} ` +
87
+ `${totalCoins > 0 ? c.green + '+⏣' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
88
+ ` ${totalCmds} cmds ${_getUptime()} ${memMB}MB`
89
+ );
90
+ lines.push('');
91
+
92
+ // Redraw in place
93
+ process.stdout.write(`\x1b[${_lineCount}A`);
94
+ process.stdout.write(lines.join('\n') + '\n');
95
+ _lineCount = lines.length - 1;
96
+ }
97
+
98
+ function _clear() {
99
+ if (_lineCount > 0) {
100
+ process.stdout.write(`\x1b[${_lineCount}A`);
101
+ for (let i = 0; i < _lineCount; i++) {
102
+ process.stdout.write(`\x1b[2K\r${i < _lineCount - 1 ? '\x1b[1A' : ''}`);
103
+ }
104
+ _lineCount = 0;
105
+ }
106
+ }
107
+
108
+ // ── Public API ────────────────────────────────────────────────
109
+
110
+ function init({ workers, getUptime, isShuttingDown }) {
111
+ _workers = workers;
112
+ _getUptime = getUptime || (() => '0s');
113
+ _isShuttingDown = isShuttingDown || (() => false);
114
+ }
115
+
116
+ function drawBanner(version) {
117
+ const title = `DANKGRINDER v${version}`;
118
+ const gradient = gradientLine(title, [77, 142, 255], [255, 92, 147]);
119
+ console.log('');
120
+ console.log(` ${gradient}${c.reset}`);
121
+ console.log(` ${DIM}Ctrl+C to stop${c.reset}`);
122
+ console.log('');
123
+ }
124
+
125
+ function start() {
126
+ if (_workers.length > 30) return; // too many accounts — stay quiet
127
+ _lineCount = 0;
128
+ _render();
129
+ _interval = setInterval(() => {
130
+ if (_isShuttingDown()) {
131
+ clearInterval(_interval);
132
+ _interval = null;
133
+ return;
134
+ }
135
+ _render();
136
+ }, 10_000);
137
+ }
138
+
139
+ function stop() {
140
+ if (_interval) { clearInterval(_interval); _interval = null; }
141
+ _clear();
142
+ }
143
+
144
+ module.exports = { init, drawBanner, start, stop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.28.0",
3
+ "version": "8.32.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"