dankgrinder 7.69.0 → 7.72.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,
@@ -133,10 +132,25 @@ const CLUSTER_PREFIX = 'dkg:cluster:';
133
132
  function initRedis() {
134
133
  if (!redis && REDIS_URL) {
135
134
  try {
136
- redis = new Redis(REDIS_URL, { maxRetriesPerRequest: null, lazyConnect: true });
137
- redis.connect().catch(() => {});
135
+ redis = new Redis(REDIS_URL, {
136
+ maxRetriesPerRequest: 3,
137
+ retryStrategy: (times) => times > 5 ? null : Math.min(times * 500, 3000),
138
+ lazyConnect: true,
139
+ });
140
+ redis.connect().catch((e) => {
141
+ // Only warn once — don't spam on persistent connection failures
142
+ if (!redis || redis.status === 'wait') {
143
+ console.warn(`[Redis] connection failed: ${e.message} — continuing without Redis`);
144
+ }
145
+ });
146
+ redis.on('error', (e) => {
147
+ // Suppress common transient errors from spamming stderr
148
+ const msg = e?.message || '';
149
+ if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('connect')) return;
150
+ console.error(`[Redis] error: ${msg}`);
151
+ });
138
152
  } catch (e) {
139
- console.error('Redis connection failed', e);
153
+ // Redis optional continue without it
140
154
  }
141
155
  }
142
156
  }
@@ -321,123 +335,17 @@ function colorBanner() {
321
335
  return out;
322
336
  }
323
337
 
324
- // ── Live Dashboard State ─────────────────────────────────────
325
- let dashboardLines = 0;
326
- let dashboardStarted = false;
327
- let dashboardRendering = false;
328
- let lastRenderTime = 0;
329
- let renderPending = false;
330
- let totalBalance = 0;
331
- let totalCoins = 0;
332
- let totalCommands = 0;
333
- let startTime = Date.now();
334
- let shutdownCalled = false;
335
- let sessionPeakCoins = 0;
336
- let isNewHigh = false;
337
- // RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
338
- const recentLogs = new RingBuffer(8);
339
- const MAX_LOGS = 8;
340
- const RENDER_THROTTLE_MS = 200;
341
- // Earnings history for sparkline (sample every 10 seconds)
342
- const earningsHistory = new RingBuffer(30);
343
- let lastEarningsSample = 0;
344
- // Per-command stats tracking
345
- const cmdStats = new Map();
346
- // Coins per minute history for rate graph
347
- const cpmHistory = new RingBuffer(20);
348
- let lastCpmSample = 0;
349
-
350
- function formatUptime() {
351
- const s = Math.floor((Date.now() - startTime) / 1000);
352
- const h = Math.floor(s / 3600);
353
- const m = Math.floor((s % 3600) / 60);
354
- const sec = s % 60;
355
- if (h > 0) return `${h}h ${m}m ${sec}s`;
356
- if (m > 0) return `${m}m ${sec}s`;
357
- return `${sec}s`;
358
- }
359
-
360
- function formatCoins(n) {
361
- if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
362
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
363
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
364
- return n.toLocaleString();
365
- }
366
-
367
- function scheduleRender() {
368
- if (renderPending || !dashboardStarted) return;
369
- const now = Date.now();
370
- const elapsed = now - lastRenderTime;
371
- if (elapsed >= RENDER_THROTTLE_MS) {
372
- renderDashboard();
373
- } else {
374
- renderPending = true;
375
- setTimeout(() => { renderPending = false; renderDashboard(); }, RENDER_THROTTLE_MS - elapsed);
376
- }
377
- }
378
-
379
- // ── Dashboard ──────────────────────────────────────────────────────────────────
380
- // Thin wrapper: aggregates stats then delegates to ./dashboard.js
381
- function renderDashboard() {
382
- if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
383
- dashboardRendering = true;
384
- lastRenderTime = Date.now();
385
-
386
- // Aggregate session totals
387
- totalBalance = 0; totalCoins = 0; totalCommands = 0;
388
- let totalErrors = 0;
389
- for (const w of workers) {
390
- totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
391
- totalCoins += w.stats.coins || 0;
392
- totalCommands += w.stats.commands || 0;
393
- totalErrors += w.stats.errors || 0;
394
- }
395
- if (totalCoins > sessionPeakCoins) {
396
- sessionPeakCoins = totalCoins;
397
- isNewHigh = true;
398
- setTimeout(() => { isNewHigh = false; }, 3000);
399
- }
400
-
401
- // Pass all state into the dashboard module
402
- const newLines = renderDashboardImpl({
403
- workers, dashboardStarted, dashboardRendering, dashboardLines,
404
- totalBalance, totalCoins, totalCommands, startTime,
405
- sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
406
- earningsHistory, lastEarningsSample,
407
- CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
408
- AccountWorker, PULSE_CHARS, getSpinner, gradientText,
409
- rgb, c, BOX,
410
- });
411
-
412
- if (newLines != null) dashboardLines = Math.max(dashboardLines, newLines);
413
- dashboardRendering = false;
414
- }
415
-
338
+ // ── Simple Logging ─────────────────────────────────────────────
416
339
  function log(type, msg, label) {
417
- const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
418
- const icons = {
419
- info: '·', success: '✓', error: '✗', warn: '!',
420
- 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}`,
421
346
  };
422
- const tagRaw = label ? label.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 12) : '';
423
- const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
424
- const tw = Math.max(process.stdout.columns || 80, 60);
425
- if (dashboardStarted) {
426
- const maxLen = tw - 8;
427
- const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
428
- recentLogs.push(entry.substring(0, maxLen));
429
- scheduleRender();
430
- } else {
431
- const colorIcons = {
432
- info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
433
- error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
434
- cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
435
- buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
436
- debug: `${c.dim}·${c.reset}`,
437
- };
438
- const tagCol = label ? `${label} ` : '';
439
- console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
440
- }
347
+ const tagCol = label ? `${label} ` : '';
348
+ console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
441
349
  }
442
350
 
443
351
  async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
@@ -869,7 +777,6 @@ class AccountWorker {
869
777
 
870
778
  setStatus(text) {
871
779
  this.lastStatus = stripAnsi(String(text || '')).replace(/\s+/g, ' ').trim();
872
- if (dashboardStarted) scheduleRender();
873
780
  }
874
781
 
875
782
  waitForDankMemer(timeout = 15000) {
@@ -1485,20 +1392,19 @@ class AccountWorker {
1485
1392
  if (currentLevel > 0) {
1486
1393
  await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1487
1394
  this._level = currentLevel;
1488
- // Only log to terminal during startup — after dashboardStarted, go to live feed
1489
- if (dashboardStarted) this.log('info', `DM level: ${c.bold}${currentLevel}${c.reset}`);
1490
- }
1491
- if (lastLifesaverCount >= 0) {
1492
- await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
1493
- this._lifesavers = lastLifesaverCount;
1494
- if (lastLifesaverCount === 0) {
1495
- await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1496
- await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1497
- if (dashboardStarted) this.log('error', `${c.red}0 LIFESAVERS! Crime/Search will be disabled.${c.reset}`);
1395
+ if (currentLevel > 0) {
1396
+ await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1397
+ this._level = currentLevel;
1398
+ }
1399
+ if (lastLifesaverCount >= 0) {
1400
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
1401
+ this._lifesavers = lastLifesaverCount;
1402
+ if (lastLifesaverCount === 0) {
1403
+ await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1404
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1405
+ }
1498
1406
  }
1499
1407
  }
1500
- }
1501
-
1502
1408
  return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1503
1409
  } catch (e) {
1504
1410
  lastError = e;
@@ -1507,7 +1413,6 @@ class AccountWorker {
1507
1413
  }
1508
1414
  }
1509
1415
  }
1510
- if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1511
1416
  return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1512
1417
  }
1513
1418
 
@@ -2872,8 +2777,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2872
2777
  await new Promise((r) => setTimeout(r, 10000));
2873
2778
  data = await fetchConfig(4, 2000, fetchOpts);
2874
2779
  }
2875
- console.log(`[DEBUG] fetched config, accounts: ${data?.accounts?.length || 0}`);
2876
-
2877
2780
  if (data && data.error) {
2878
2781
  log('error', `${data.error}`);
2879
2782
  return;
@@ -2957,13 +2860,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2957
2860
  }
2958
2861
  }
2959
2862
  }
2960
- scheduleRender();
2961
2863
  }
2962
2864
 
2963
2865
  if (event.type === 'levelup') {
2964
2866
  if (event.to > 0) {
2965
2867
  w._level = event.to;
2966
- scheduleRender();
2967
2868
  }
2968
2869
  }
2969
2870
  }
@@ -3317,13 +3218,10 @@ async function start(apiKey, apiUrl, opts = {}) {
3317
3218
  const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3318
3219
  parts.push(`${D}${pulse}♥?${c.reset}`);
3319
3220
  }
3320
- if (parts.length > 0) {
3321
- recentLogs.push({ ts: Date.now(), username: w.username, color: w.color, command: 'dm check', response: parts.join(' '), status: 'ok' });
3322
- }
3323
3221
  } catch {}
3324
3222
  }
3325
3223
  if (dmNoLs.length > 0) {
3326
- 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(', ')}`);
3327
3225
  // Set Redis keys to block crime/search
3328
3226
  for (const w of activeWorkers) {
3329
3227
  if (dmNoLs.includes(w.username) && redis) {
@@ -3335,7 +3233,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3335
3233
  }
3336
3234
  }
3337
3235
  if (dmUnknown.length > 0) {
3338
- 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(', ')}`);
3339
3237
  // Crime/search on these accounts will be skipped via safety hold until the live
3340
3238
  // DM gateway listener detects a death (→ sets count) or confirms clean.
3341
3239
  }
@@ -3343,41 +3241,18 @@ async function start(apiKey, apiUrl, opts = {}) {
3343
3241
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3344
3242
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3345
3243
  if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
3346
- 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'}`);
3347
3245
  console.log('');
3348
3246
 
3349
3247
  console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
3350
3248
 
3351
- console.log(`[DEBUG] activeWorkers count: ${activeWorkers.length}`);
3352
- for (let i = 0; i < Math.min(activeWorkers.length, 5); i++) {
3353
- const w = activeWorkers[i];
3354
- console.log(`[DEBUG] worker[${i}]: ${w.username}, running=${w.running}, client=${!!w.client}, channel=${!!w.channel}`);
3355
- }
3356
-
3357
3249
  // Phase 3: Start all grind loops (only for valid workers)
3358
3250
  for (const w of activeWorkers) {
3359
- console.log(`[DEBUG] calling grindLoop on: ${w.username}`);
3360
3251
  if (!shutdownCalled) w.grindLoop();
3361
3252
  }
3362
3253
 
3363
- startTime = Date.now();
3364
- console.log(`[DEBUG] dashboardStarted set to true`);
3365
- dashboardStarted = true;
3366
- setDashboardActive(true);
3367
- // Setup keyboard shortcuts
3368
- setupKeyboardShortcuts();
3369
- // Cursor hide to reduce visual noise during renders
3370
- process.stdout.write(c.hide);
3371
-
3372
- // Re-render on terminal resize so layout adapts to window size
3373
- process.stdout.on('resize', () => {
3374
- process.stdout.write('\x1b[2J\x1b[H');
3375
- dashboardLines = 0;
3376
- scheduleRender();
3377
- });
3378
-
3379
- setInterval(() => scheduleRender(), 1000);
3380
- 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`);
3381
3256
 
3382
3257
  // Cluster heartbeat — lets other nodes see this node is alive
3383
3258
  if (CLUSTER_ENABLED) {
@@ -3434,7 +3309,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3434
3309
  const before = workers.length;
3435
3310
  // Keep ALL workers visible — never remove from array (user wants to see gaps)
3436
3311
  // Only clean up workerMap entries for accounts fully removed from API
3437
- if (workers.length !== before) scheduleRender();
3312
+ if (workers.length !== before) { /* workers changed */ }
3438
3313
  } catch {}
3439
3314
  }, 10_000);
3440
3315
 
@@ -3443,18 +3318,9 @@ async function start(apiKey, apiUrl, opts = {}) {
3443
3318
  if (sigintHandled) return;
3444
3319
  sigintHandled = true;
3445
3320
  shutdownCalled = true;
3446
- dashboardStarted = false;
3447
3321
  setDashboardActive(false);
3448
3322
  process.stdout.write(c.show);
3449
3323
 
3450
- if (dashboardLines > 0) {
3451
- process.stdout.write(c.cursorUp(dashboardLines));
3452
- for (let i = 0; i < dashboardLines; i++) {
3453
- process.stdout.write(c.clearLine + '\r\n');
3454
- }
3455
- process.stdout.write(c.cursorUp(dashboardLines));
3456
- }
3457
-
3458
3324
  const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3459
3325
  console.log('');
3460
3326
  console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
@@ -3516,79 +3382,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3516
3382
  }
3517
3383
 
3518
3384
  // ══════════════════════════════════════════════════════════════
3519
- // Keyboard Shortcuts (Quality of Life)
3520
- // ══════════════════════════════════════════════════════════════
3521
- // Single-key shortcuts for common actions
3522
- function setupKeyboardShortcuts() {
3523
- if (process.stdin.isTTY) {
3524
- process.stdin.setRawMode(true);
3525
- process.stdin.resume();
3526
- process.stdin.setEncoding('utf8');
3527
-
3528
- // Premium styled keyboard shortcuts with gradient box
3529
- const accent = rgb(139, 92, 246);
3530
- const dim = c.dim;
3531
- const kw = 60;
3532
- console.log('');
3533
- console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
3534
- console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
3535
- console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
3536
- 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}`);
3537
- console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
3538
- console.log('');
3539
-
3540
- process.stdin.on('data', (key) => {
3541
- const k = key.toString().toLowerCase();
3542
-
3543
- // Ctrl+C or q = quit
3544
- if (k === '\u0003' || k === 'q') {
3545
- process.stdout.write(c.show);
3546
- console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
3547
- process.emit('SIGINT');
3548
- return;
3549
- }
3550
-
3551
- // p = pause all accounts
3552
- if (k === 'p') {
3553
- let count = 0;
3554
- workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
3555
- recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
3556
- scheduleRender();
3557
- return;
3558
- }
3559
-
3560
- // r = resume all accounts
3561
- if (k === 'r') {
3562
- let count = 0;
3563
- workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
3564
- recentLogs.push(`>> RESUMED ${count} accounts`);
3565
- scheduleRender();
3566
- return;
3567
- }
3568
-
3569
- // s = show status summary (pushed to log feed)
3570
- if (k === 's') {
3571
- const active = workers.filter(w => w.running && !w.paused).length;
3572
- const paused = workers.filter(w => w.paused).length;
3573
- const invalid = workers.filter(w => w._tokenInvalid).length;
3574
- const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
3575
- const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
3576
- const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
3577
- recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
3578
- recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
3579
- scheduleRender();
3580
- return;
3581
- }
3582
-
3583
- // ? or h = show help
3584
- if (k === '?' || k === 'h') {
3585
- recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
3586
- scheduleRender();
3587
- return;
3588
- }
3589
- });
3590
- }
3591
- }
3385
+ // Keyboard shortcuts removed no display to update
3592
3386
 
3593
3387
  // Export the start function for CLI
3594
3388
  module.exports = { start };
package/lib/rawLogger.js CHANGED
@@ -54,7 +54,10 @@ async function init(redisUrl) {
54
54
  redisReady = true;
55
55
  console.log('[rawLogger] Redis connected');
56
56
  redis.on('error', (e) => {
57
- console.error(`[rawLogger] Redis error: ${e.message}`);
57
+ // Suppress common transient network errors from spamming stderr
58
+ const msg = e?.message || '';
59
+ if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('read') || msg.includes('connect')) return;
60
+ console.error(`[rawLogger] Redis error: ${msg}`);
58
61
  redisReady = false;
59
62
  });
60
63
  redis.on('close', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.69.0",
3
+ "version": "7.72.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"