dankgrinder 7.71.0 → 7.73.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/lib/dashboard.js CHANGED
@@ -1,271 +1,6 @@
1
1
  /**
2
- * DankGrinder CLI Dashboard
3
- *
4
- * Fresh rewrite. Simple line-by-line overwrite no full clear, no cursor tricks.
5
- * Every render writes N rows from line 1 onward. Previous content is overwritten.
6
- *
7
- * Context:
8
- * workers, dashboardStarted, dashboardRendering, dashboardLines,
9
- * totalBalance, totalCoins, totalCommands, startTime,
10
- * sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
11
- * CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
12
- * AccountWorker, PULSE_CHARS, getSpinner, gradientText,
13
- * rgb, c
2
+ * CLI Dashboard — REMOVED
3
+ * Dashboard functionality has been removed from the CLI.
4
+ * All startup output goes through simple console.log.
14
5
  */
15
-
16
- 'use strict';
17
-
18
- // Local refs (set per render)
19
- let _c, _rgb, _spinnerFn, _gradientFn;
20
-
21
- function init(ctx) {
22
- _c = ctx.c;
23
- _rgb = ctx.rgb;
24
- _spinnerFn = ctx.getSpinner;
25
- _gradientFn = ctx.gradientText;
26
- }
27
-
28
- // ── Helpers ─────────────────────────────────────────────────────
29
-
30
- function vis(s) {
31
- return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
32
- }
33
-
34
- function pad(str, width) {
35
- return str + ' '.repeat(Math.max(0, width - vis(str)));
36
- }
37
-
38
- function clearLine(content) {
39
- return `${_c.clearLine}\r${content}`;
40
- }
41
-
42
- function fmtCoins(n) {
43
- if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
44
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
45
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
46
- return n.toLocaleString();
47
- }
48
-
49
- function fmtUptime(startTime) {
50
- const s = Math.floor((Date.now() - startTime) / 1000);
51
- const h = Math.floor(s / 3600);
52
- const m = Math.floor((s % 3600) / 60);
53
- const sec = s % 60;
54
- if (h > 0) return `${h}h ${m}m ${sec}s`;
55
- if (m > 0) return `${m}m ${sec}s`;
56
- return `${sec}s`;
57
- }
58
-
59
- // ── Render ──────────────────────────────────────────────────────
60
-
61
- function renderDashboard(ctx) {
62
- if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering) return;
63
-
64
- init(ctx);
65
-
66
- const tw = Math.max(process.stdout.columns || 80, 60);
67
- const _ = _c.reset;
68
-
69
- // Color shortcuts
70
- const A = _rgb(139, 92, 246);
71
- const G = _rgb(52, 211, 153);
72
- const Au = _rgb(255, 215, 0);
73
- const O = _rgb(251, 146, 60);
74
- const Cy = _rgb(34, 211, 238);
75
- const R = _rgb(239, 68, 68);
76
- const Y = _rgb(251, 191, 36);
77
- const W = _c.white;
78
- const D = _c.dim;
79
-
80
- // ── Aggregate stats ───────────────────────────────────────────
81
- let aggBalance = 0, aggCoins = 0, aggCmds = 0, aggErrors = 0;
82
- for (const w of ctx.workers) {
83
- aggBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
84
- aggCoins += w.stats.coins || 0;
85
- aggCmds += w.stats.commands || 0;
86
- aggErrors += w.stats.errors || 0;
87
- }
88
- const successRate = aggCmds > 0 ? Math.round(((aggCmds - aggErrors) / aggCmds) * 100) : 100;
89
- const elapsedHrs = (Date.now() - ctx.startTime) / 3_600_000;
90
- const perHr = elapsedHrs > 0.01 ? Math.round(aggCoins / elapsedHrs) : 0;
91
- const cpmVal = ctx.globalCmdRate.getRate().toFixed(1);
92
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
93
- const spin = _spinnerFn('braille');
94
-
95
- const activeCount = ctx.workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
96
- const invalidCount = ctx.workers.filter(w => w._tokenInvalid).length;
97
- const pausedCount = ctx.workers.filter(w => w.paused || w.dashboardPaused).length;
98
- const recovCount = ctx.workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
99
-
100
- const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
101
-
102
- // ── Column layout ─────────────────────────────────────────────
103
- const colNum = 4;
104
- const colSts = 3;
105
- const colName = Math.max(16, Math.min(22, Math.floor(tw * 0.22)));
106
- const colBal = 10;
107
- const colLvl = 5;
108
- const colLs = 4;
109
- const colEarn = 9;
110
- const colAct = Math.max(8, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 12);
111
- const gap = ' ';
112
-
113
- // ── Helpers to build rows ─────────────────────────────────────
114
- const border = (char) => `${A}${char}${D}${'─'.repeat(tw - 2)}${A}${char}${_}`;
115
-
116
- function mkRow(content, leftPad = ' ') {
117
- const raw = content.replace(/\x1b\[[0-9;]*m/g, '');
118
- const padLen = Math.max(0, tw - 4 - raw.length);
119
- return `${A}│${_}${leftPad}${content}${' '.repeat(padLen)}${leftPad}${A}│${_}`;
120
- }
121
-
122
- // ── Build all rows ────────────────────────────────────────────
123
- const rows = [];
124
-
125
- // HEADER
126
- const title = _c.bold + _gradientFn(' DANK GRINDER ', [139, 92, 246], [52, 211, 153]) + _;
127
- const h1 = [
128
- title,
129
- `${D}v${ctx.PKG_VERSION}${_}`,
130
- `${G}${spin}${_}`,
131
- `${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_}`,
132
- `${D}bal${_} ${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_}`,
133
- `${G}+⏣${fmtCoins(perHr)}${_}/h`,
134
- `${G}${activeCount}${_}/${W}${ctx.workers.length}${_} active`,
135
- invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
136
- pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
137
- recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
138
- ` ${D}${aggCmds}${_}cmds`,
139
- ` ${D}${cpmVal}${_}/min`,
140
- ` ${D}${memMB}MB`,
141
- modeTag,
142
- ].filter(Boolean).join(' ');
143
-
144
- rows.push(border('╔'));
145
- rows.push(mkRow(h1));
146
- rows.push(border('╠'));
147
- rows.push(mkRow(`${D}session${_} ${G}${fmtCoins(aggCoins)}${_} coins ${G}${successRate}%${_} success ${W}${fmtCoins(ctx.sessionPeakCoins)}${_} peak${ctx.isNewHigh ? ` ${Au}★ NEW HIGH${_}` : ''}`));
148
- rows.push(border('╚'));
149
-
150
- // ACCOUNTS TABLE
151
- const headers = [
152
- `${D}${pad('#', colNum)}${_}`,
153
- `${D}${pad('S', colSts)}${_}`,
154
- `${_gradientFn(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
155
- `${D}${pad('Balance', colBal)}${_}`,
156
- `${D}${pad('Lvl', colLvl)}${_}`,
157
- `${D}${pad('LS', colLs)}${_}`,
158
- `${D}${pad('Earned', colEarn)}${_}`,
159
- `${D}${pad('Activity', colAct)}${_}`,
160
- ].join(gap);
161
-
162
- rows.push(border('╔'));
163
- rows.push(mkRow(headers));
164
- rows.push(border('╟'));
165
-
166
- const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
167
- const maxRows = Math.max(5, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 12)));
168
- const visible = sorted.slice(0, maxRows);
169
- const extraRows = sorted.length - maxRows;
170
-
171
- for (const wk of visible) {
172
- const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
173
-
174
- let stsIcon;
175
- if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
176
- else if (!wk.running) stsIcon = `${D}○${_}`;
177
- else if (isRecov) stsIcon = `${O}${_spinnerFn('braille').substring(0, 1)}${_}`;
178
- else if (wk.paused) stsIcon = `${R}⏸${_}`;
179
- else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
180
- else if (wk.busy) stsIcon = `${G}${_spinnerFn('pulse').substring(0, 1)}${_}`;
181
- else stsIcon = `${G}●${_}`;
182
-
183
- const name = (wk.username || wk.account.label || '?').substring(0, colName);
184
- const nameStr = `${wk.color}${name}${_}`;
185
- const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
186
- const lvl = wk._level || 0;
187
- const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
188
- const ls = wk._lifesavers;
189
- let lsStr;
190
- if (ls === 0) lsStr = `${R}♥${ls}${_}`;
191
- else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
192
- else if (ls != null) lsStr = `${G}♥${ls}${_}`;
193
- else {
194
- const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
195
- lsStr = `${D}${p}♥?${_}`;
196
- }
197
-
198
- const earn = wk.stats.coins || 0;
199
- const earnStr = earn > 0 ? `${G}+${fmtCoins(earn)}${_}` : `${D}────${_}`;
200
- const actRaw = (wk.lastStatus || 'ready').replace(/\x1b\[[0-9;]*m/g, '').substring(0, colAct);
201
- const actStr = `${D}${pad(actRaw, colAct)}${_}`;
202
- const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
203
-
204
- const rowStr = [
205
- numStr,
206
- pad(stsIcon, colSts),
207
- pad(nameStr, colName),
208
- pad(balStr, colBal),
209
- pad(lvlStr, colLvl),
210
- pad(lsStr, colLs),
211
- pad(earnStr, colEarn),
212
- actStr,
213
- ].join(gap);
214
-
215
- rows.push(mkRow(rowStr));
216
- }
217
-
218
- if (extraRows > 0) {
219
- rows.push(mkRow(`${D}+${extraRows} more${_}`));
220
- }
221
-
222
- rows.push(border('╚'));
223
-
224
- // LIVE FEED
225
- const logs = ctx.recentLogs.toArray();
226
- if (logs.length > 0) {
227
- const feedTitle = `${_gradientFn(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${_spinnerFn('pulse')}${_}`;
228
- rows.push(border('╔'));
229
- rows.push(mkRow(feedTitle));
230
- rows.push(border('╟'));
231
-
232
- for (const entry of logs) {
233
- let lineText;
234
- if (typeof entry === 'string') {
235
- lineText = entry;
236
- } else if (entry && typeof entry === 'object') {
237
- const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false }) : '';
238
- const user = entry.username ? String(entry.username) : '';
239
- const cmd = entry.command ? `[${entry.command}]` : '';
240
- const resp = entry.response || '';
241
- lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
242
- } else {
243
- lineText = String(entry);
244
- }
245
- rows.push(mkRow(`${D}${lineText}${_}`));
246
- }
247
- rows.push(border('╚'));
248
- }
249
-
250
- // FOOTER
251
- rows.push(border('╔'));
252
- rows.push(mkRow(`${modeTag} ${D}P=pause R=resume S=status Q=quit${_}`));
253
- rows.push(border('╚'));
254
-
255
- // ── Flush: overwrite rows from line 1, then blank remaining screen ──
256
- const rowsOnScreen = process.stdout.rows || 24;
257
- for (const row of rows) {
258
- process.stdout.write(`${_c.clearLine}\r${row}\n`);
259
- }
260
- // Fill remaining screen lines with blanks so old startup logs don't bleed through
261
- const extra = rowsOnScreen - rows.length;
262
- for (let i = 0; i < extra; i++) {
263
- process.stdout.write(`${_c.clearLine}\r${' '.repeat(tw)}\n`);
264
- }
265
- // Move cursor to end so terminal doesn't scroll on next \r write
266
- process.stdout.write(`\x1b[${rows.length};1H`);
267
-
268
- return rows.length;
269
- }
270
-
271
- module.exports = { renderDashboard };
6
+ module.exports = {};
package/lib/grinder.js CHANGED
@@ -3,7 +3,6 @@ const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
4
  const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
5
5
  const rawLogger = require('./rawLogger');
6
- const { renderDashboard: renderDashboardImpl } = require('./dashboard');
7
6
  const {
8
7
  BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
9
8
  AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
@@ -336,123 +335,17 @@ function colorBanner() {
336
335
  return out;
337
336
  }
338
337
 
339
- // ── Live Dashboard State ─────────────────────────────────────
340
- let dashboardLines = 0;
341
- let dashboardStarted = false;
342
- let dashboardRendering = false;
343
- let lastRenderTime = 0;
344
- let renderPending = false;
345
- let totalBalance = 0;
346
- let totalCoins = 0;
347
- let totalCommands = 0;
348
- let startTime = Date.now();
349
- let shutdownCalled = false;
350
- let sessionPeakCoins = 0;
351
- let isNewHigh = false;
352
- // RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
353
- const recentLogs = new RingBuffer(8);
354
- const MAX_LOGS = 8;
355
- const RENDER_THROTTLE_MS = 200;
356
- // Earnings history for sparkline (sample every 10 seconds)
357
- const earningsHistory = new RingBuffer(30);
358
- let lastEarningsSample = 0;
359
- // Per-command stats tracking
360
- const cmdStats = new Map();
361
- // Coins per minute history for rate graph
362
- const cpmHistory = new RingBuffer(20);
363
- let lastCpmSample = 0;
364
-
365
- function formatUptime() {
366
- const s = Math.floor((Date.now() - startTime) / 1000);
367
- const h = Math.floor(s / 3600);
368
- const m = Math.floor((s % 3600) / 60);
369
- const sec = s % 60;
370
- if (h > 0) return `${h}h ${m}m ${sec}s`;
371
- if (m > 0) return `${m}m ${sec}s`;
372
- return `${sec}s`;
373
- }
374
-
375
- function formatCoins(n) {
376
- if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
377
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
378
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
379
- return n.toLocaleString();
380
- }
381
-
382
- function scheduleRender() {
383
- if (renderPending || !dashboardStarted) return;
384
- const now = Date.now();
385
- const elapsed = now - lastRenderTime;
386
- if (elapsed >= RENDER_THROTTLE_MS) {
387
- renderDashboard();
388
- } else {
389
- renderPending = true;
390
- setTimeout(() => { renderPending = false; renderDashboard(); }, RENDER_THROTTLE_MS - elapsed);
391
- }
392
- }
393
-
394
- // ── Dashboard ──────────────────────────────────────────────────────────────────
395
- // Thin wrapper: aggregates stats then delegates to ./dashboard.js
396
- function renderDashboard() {
397
- if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
398
- dashboardRendering = true;
399
- lastRenderTime = Date.now();
400
-
401
- // Aggregate session totals
402
- totalBalance = 0; totalCoins = 0; totalCommands = 0;
403
- let totalErrors = 0;
404
- for (const w of workers) {
405
- totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
406
- totalCoins += w.stats.coins || 0;
407
- totalCommands += w.stats.commands || 0;
408
- totalErrors += w.stats.errors || 0;
409
- }
410
- if (totalCoins > sessionPeakCoins) {
411
- sessionPeakCoins = totalCoins;
412
- isNewHigh = true;
413
- setTimeout(() => { isNewHigh = false; }, 3000);
414
- }
415
-
416
- // Pass all state into the dashboard module
417
- const newLines = renderDashboardImpl({
418
- workers, dashboardStarted, dashboardRendering, dashboardLines,
419
- totalBalance, totalCoins, totalCommands, startTime,
420
- sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
421
- earningsHistory, lastEarningsSample,
422
- CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
423
- AccountWorker, PULSE_CHARS, getSpinner, gradientText,
424
- rgb, c, BOX,
425
- });
426
-
427
- if (newLines != null) dashboardLines = Math.max(dashboardLines, newLines);
428
- dashboardRendering = false;
429
- }
430
-
338
+ // ── Simple Logging ─────────────────────────────────────────────
431
339
  function log(type, msg, label) {
432
- const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
433
- const icons = {
434
- info: '·', success: '✓', error: '✗', warn: '!',
435
- cmd: '▸', coin: '$', buy: '♦', bal: '◈', debug: '·',
340
+ const colorIcons = {
341
+ info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
342
+ error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
343
+ cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
344
+ buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
345
+ debug: `${c.dim}·${c.reset}`,
436
346
  };
437
- const tagRaw = label ? label.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 12) : '';
438
- const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
439
- const tw = Math.max(process.stdout.columns || 80, 60);
440
- if (dashboardStarted) {
441
- const maxLen = tw - 8;
442
- const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
443
- recentLogs.push(entry.substring(0, maxLen));
444
- scheduleRender();
445
- } else {
446
- const colorIcons = {
447
- info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
448
- error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
449
- cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
450
- buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
451
- debug: `${c.dim}·${c.reset}`,
452
- };
453
- const tagCol = label ? `${label} ` : '';
454
- console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
455
- }
347
+ const tagCol = label ? `${label} ` : '';
348
+ console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
456
349
  }
457
350
 
458
351
  async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
@@ -884,7 +777,6 @@ class AccountWorker {
884
777
 
885
778
  setStatus(text) {
886
779
  this.lastStatus = stripAnsi(String(text || '')).replace(/\s+/g, ' ').trim();
887
- if (dashboardStarted) scheduleRender();
888
780
  }
889
781
 
890
782
  waitForDankMemer(timeout = 15000) {
@@ -1497,34 +1389,32 @@ class AccountWorker {
1497
1389
 
1498
1390
  // Update Redis with findings
1499
1391
  if (redis) {
1500
- if (currentLevel > 0) {
1501
- await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1502
- this._level = currentLevel;
1503
- // Only log to terminal during startup — after dashboardStarted, go to live feed
1504
- if (dashboardStarted) this.log('info', `DM level: ${c.bold}${currentLevel}${c.reset}`);
1505
- }
1506
- if (lastLifesaverCount >= 0) {
1507
- await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
1508
- this._lifesavers = lastLifesaverCount;
1509
- if (lastLifesaverCount === 0) {
1510
- await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1511
- await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1512
- if (dashboardStarted) this.log('error', `${c.red}0 LIFESAVERS! Crime/Search will be disabled.${c.reset}`);
1392
+ try {
1393
+ if (currentLevel > 0) {
1394
+ await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1395
+ this._level = currentLevel;
1513
1396
  }
1514
- }
1397
+ if (lastLifesaverCount >= 0) {
1398
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
1399
+ this._lifesavers = lastLifesaverCount;
1400
+ if (lastLifesaverCount === 0) {
1401
+ await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1402
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1403
+ }
1404
+ }
1405
+ } catch { /* Redis errors non-fatal */ }
1515
1406
  }
1516
1407
 
1517
- return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1518
- } catch (e) {
1519
- lastError = e;
1520
- if (attempt < maxRetries - 1) {
1521
- await new Promise(r => setTimeout(r, delays[attempt]));
1522
- }
1408
+ return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1409
+ } catch (e) {
1410
+ lastError = e;
1411
+ if (attempt < maxRetries - 1) {
1412
+ await new Promise(r => setTimeout(r, delays[attempt]));
1523
1413
  }
1524
1414
  }
1525
- if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1526
- return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1527
1415
  }
1416
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1417
+ }
1528
1418
 
1529
1419
  // ── Run Single Command ──────────────────────────────────────
1530
1420
  // Each modular command handler sends the command, waits for response,
@@ -2970,13 +2860,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2970
2860
  }
2971
2861
  }
2972
2862
  }
2973
- scheduleRender();
2974
2863
  }
2975
2864
 
2976
2865
  if (event.type === 'levelup') {
2977
2866
  if (event.to > 0) {
2978
2867
  w._level = event.to;
2979
- scheduleRender();
2980
2868
  }
2981
2869
  }
2982
2870
  }
@@ -3330,13 +3218,10 @@ async function start(apiKey, apiUrl, opts = {}) {
3330
3218
  const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3331
3219
  parts.push(`${D}${pulse}♥?${c.reset}`);
3332
3220
  }
3333
- if (parts.length > 0) {
3334
- recentLogs.push({ ts: Date.now(), username: w.username, color: w.color, command: 'dm check', response: parts.join(' '), status: 'ok' });
3335
- }
3336
3221
  } catch {}
3337
3222
  }
3338
3223
  if (dmNoLs.length > 0) {
3339
- recentLogs.push({ ts: Date.now(), username: 'system', color: rgb(239, 68, 68), command: 'dm check', response: `⚠ No lifesavers: ${dmNoLs.join(', ')}`, status: 'warn' });
3224
+ log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3340
3225
  // Set Redis keys to block crime/search
3341
3226
  for (const w of activeWorkers) {
3342
3227
  if (dmNoLs.includes(w.username) && redis) {
@@ -3348,7 +3233,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3348
3233
  }
3349
3234
  }
3350
3235
  if (dmUnknown.length > 0) {
3351
- recentLogs.push({ ts: Date.now(), username: 'system', color: rgb(251, 191, 36), command: 'dm check', response: `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`, status: 'warn' });
3236
+ log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
3352
3237
  // Crime/search on these accounts will be skipped via safety hold until the live
3353
3238
  // DM gateway listener detects a death (→ sets count) or confirms clean.
3354
3239
  }
@@ -3356,7 +3241,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3356
3241
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3357
3242
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3358
3243
  if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
3359
- recentLogs.push({ ts: Date.now(), username: 'system', color: rgb(52, 211, 153), command: 'dm check', response: dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups', status: 'ok' });
3244
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups'}`);
3360
3245
  console.log('');
3361
3246
 
3362
3247
  console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
@@ -3366,25 +3251,8 @@ async function start(apiKey, apiUrl, opts = {}) {
3366
3251
  if (!shutdownCalled) w.grindLoop();
3367
3252
  }
3368
3253
 
3369
- startTime = Date.now();
3370
- dashboardStarted = true;
3371
- setDashboardActive(true);
3372
-
3373
- // Clear screen and position cursor at top-left before dashboard takes over
3374
- process.stdout.write('\x1b[2J\x1b[H');
3375
-
3376
- // Setup keyboard shortcuts
3377
- setupKeyboardShortcuts();
3378
-
3379
- // Re-render on terminal resize so layout adapts to window size
3380
- process.stdout.on('resize', () => {
3381
- process.stdout.write('\x1b[2J\x1b[H');
3382
- dashboardLines = 0;
3383
- scheduleRender();
3384
- });
3385
-
3386
- setInterval(() => scheduleRender(), 1000);
3387
- scheduleRender();
3254
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
3255
+ console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
3388
3256
 
3389
3257
  // Cluster heartbeat — lets other nodes see this node is alive
3390
3258
  if (CLUSTER_ENABLED) {
@@ -3441,7 +3309,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3441
3309
  const before = workers.length;
3442
3310
  // Keep ALL workers visible — never remove from array (user wants to see gaps)
3443
3311
  // Only clean up workerMap entries for accounts fully removed from API
3444
- if (workers.length !== before) scheduleRender();
3312
+ if (workers.length !== before) { /* workers changed */ }
3445
3313
  } catch {}
3446
3314
  }, 10_000);
3447
3315
 
@@ -3450,18 +3318,9 @@ async function start(apiKey, apiUrl, opts = {}) {
3450
3318
  if (sigintHandled) return;
3451
3319
  sigintHandled = true;
3452
3320
  shutdownCalled = true;
3453
- dashboardStarted = false;
3454
3321
  setDashboardActive(false);
3455
3322
  process.stdout.write(c.show);
3456
3323
 
3457
- if (dashboardLines > 0) {
3458
- process.stdout.write(c.cursorUp(dashboardLines));
3459
- for (let i = 0; i < dashboardLines; i++) {
3460
- process.stdout.write(c.clearLine + '\r\n');
3461
- }
3462
- process.stdout.write(c.cursorUp(dashboardLines));
3463
- }
3464
-
3465
3324
  const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3466
3325
  console.log('');
3467
3326
  console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
@@ -3523,79 +3382,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3523
3382
  }
3524
3383
 
3525
3384
  // ══════════════════════════════════════════════════════════════
3526
- // Keyboard Shortcuts (Quality of Life)
3527
- // ══════════════════════════════════════════════════════════════
3528
- // Single-key shortcuts for common actions
3529
- function setupKeyboardShortcuts() {
3530
- if (process.stdin.isTTY) {
3531
- process.stdin.setRawMode(true);
3532
- process.stdin.resume();
3533
- process.stdin.setEncoding('utf8');
3534
-
3535
- // Premium styled keyboard shortcuts with gradient box
3536
- const accent = rgb(139, 92, 246);
3537
- const dim = c.dim;
3538
- const kw = 60;
3539
- console.log('');
3540
- console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
3541
- console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
3542
- console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
3543
- console.log(` ${accent}│${c.reset} ${rgb(96, 165, 250)}P${c.reset} ${dim}Pause all${c.reset} ${rgb(52, 211, 153)}R${c.reset} ${dim}Resume all${c.reset} ${rgb(251, 191, 36)}S${c.reset} ${dim}Status${c.reset} ${rgb(239, 68, 68)}Q${c.reset} ${dim}Quit${c.reset}${' '.repeat(Math.max(0, kw - 54))}${accent}│${c.reset}`);
3544
- console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
3545
- console.log('');
3546
-
3547
- process.stdin.on('data', (key) => {
3548
- const k = key.toString().toLowerCase();
3549
-
3550
- // Ctrl+C or q = quit
3551
- if (k === '\u0003' || k === 'q') {
3552
- process.stdout.write(c.show);
3553
- console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
3554
- process.emit('SIGINT');
3555
- return;
3556
- }
3557
-
3558
- // p = pause all accounts
3559
- if (k === 'p') {
3560
- let count = 0;
3561
- workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
3562
- recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
3563
- scheduleRender();
3564
- return;
3565
- }
3566
-
3567
- // r = resume all accounts
3568
- if (k === 'r') {
3569
- let count = 0;
3570
- workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
3571
- recentLogs.push(`>> RESUMED ${count} accounts`);
3572
- scheduleRender();
3573
- return;
3574
- }
3575
-
3576
- // s = show status summary (pushed to log feed)
3577
- if (k === 's') {
3578
- const active = workers.filter(w => w.running && !w.paused).length;
3579
- const paused = workers.filter(w => w.paused).length;
3580
- const invalid = workers.filter(w => w._tokenInvalid).length;
3581
- const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
3582
- const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
3583
- const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
3584
- recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
3585
- recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
3586
- scheduleRender();
3587
- return;
3588
- }
3589
-
3590
- // ? or h = show help
3591
- if (k === '?' || k === 'h') {
3592
- recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
3593
- scheduleRender();
3594
- return;
3595
- }
3596
- });
3597
- }
3598
- }
3385
+ // Keyboard shortcuts removed no display to update
3599
3386
 
3600
3387
  // Export the start function for CLI
3601
3388
  module.exports = { start };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.71.0",
3
+ "version": "7.73.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"