dankgrinder 8.1.0 → 8.10.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
 
@@ -2955,13 +2860,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2955
2860
  }
2956
2861
  }
2957
2862
  }
2958
- scheduleRender();
2959
2863
  }
2960
2864
 
2961
2865
  if (event.type === 'levelup') {
2962
2866
  if (event.to > 0) {
2963
2867
  w._level = event.to;
2964
- scheduleRender();
2965
2868
  }
2966
2869
  }
2967
2870
  }
@@ -3315,13 +3218,10 @@ async function start(apiKey, apiUrl, opts = {}) {
3315
3218
  const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3316
3219
  parts.push(`${D}${pulse}♥?${c.reset}`);
3317
3220
  }
3318
- if (parts.length > 0) {
3319
- recentLogs.push({ ts: Date.now(), username: w.username, color: w.color, command: 'dm check', response: parts.join(' '), status: 'ok' });
3320
- }
3321
3221
  } catch {}
3322
3222
  }
3323
3223
  if (dmNoLs.length > 0) {
3324
- 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(', ')}`);
3325
3225
  // Set Redis keys to block crime/search
3326
3226
  for (const w of activeWorkers) {
3327
3227
  if (dmNoLs.includes(w.username) && redis) {
@@ -3333,7 +3233,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3333
3233
  }
3334
3234
  }
3335
3235
  if (dmUnknown.length > 0) {
3336
- 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(', ')}`);
3337
3237
  // Crime/search on these accounts will be skipped via safety hold until the live
3338
3238
  // DM gateway listener detects a death (→ sets count) or confirms clean.
3339
3239
  }
@@ -3341,37 +3241,18 @@ async function start(apiKey, apiUrl, opts = {}) {
3341
3241
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3342
3242
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3343
3243
  if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
3344
- 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'}`);
3345
3245
  console.log('');
3346
3246
 
3347
3247
  console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
3348
3248
 
3349
- // DEBUG: activeWorkers confirmed
3350
-
3351
3249
  // Phase 3: Start all grind loops (only for valid workers)
3352
3250
  for (const w of activeWorkers) {
3353
3251
  if (!shutdownCalled) w.grindLoop();
3354
3252
  }
3355
3253
 
3356
- startTime = Date.now();
3357
- dashboardStarted = true;
3358
- setDashboardActive(true);
3359
-
3360
- // Clear screen and position cursor at top-left before dashboard takes over
3361
- process.stdout.write('\x1b[2J\x1b[H');
3362
-
3363
- // Setup keyboard shortcuts
3364
- setupKeyboardShortcuts();
3365
-
3366
- // Re-render on terminal resize so layout adapts to window size
3367
- process.stdout.on('resize', () => {
3368
- process.stdout.write('\x1b[2J\x1b[H');
3369
- dashboardLines = 0;
3370
- scheduleRender();
3371
- });
3372
-
3373
- setInterval(() => scheduleRender(), 1000);
3374
- 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`);
3375
3256
 
3376
3257
  // Cluster heartbeat — lets other nodes see this node is alive
3377
3258
  if (CLUSTER_ENABLED) {
@@ -3428,7 +3309,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3428
3309
  const before = workers.length;
3429
3310
  // Keep ALL workers visible — never remove from array (user wants to see gaps)
3430
3311
  // Only clean up workerMap entries for accounts fully removed from API
3431
- if (workers.length !== before) scheduleRender();
3312
+ if (workers.length !== before) { /* workers changed */ }
3432
3313
  } catch {}
3433
3314
  }, 10_000);
3434
3315
 
@@ -3437,18 +3318,9 @@ async function start(apiKey, apiUrl, opts = {}) {
3437
3318
  if (sigintHandled) return;
3438
3319
  sigintHandled = true;
3439
3320
  shutdownCalled = true;
3440
- dashboardStarted = false;
3441
3321
  setDashboardActive(false);
3442
3322
  process.stdout.write(c.show);
3443
3323
 
3444
- if (dashboardLines > 0) {
3445
- process.stdout.write(c.cursorUp(dashboardLines));
3446
- for (let i = 0; i < dashboardLines; i++) {
3447
- process.stdout.write(c.clearLine + '\r\n');
3448
- }
3449
- process.stdout.write(c.cursorUp(dashboardLines));
3450
- }
3451
-
3452
3324
  const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3453
3325
  console.log('');
3454
3326
  console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
@@ -3510,79 +3382,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3510
3382
  }
3511
3383
 
3512
3384
  // ══════════════════════════════════════════════════════════════
3513
- // Keyboard Shortcuts (Quality of Life)
3514
- // ══════════════════════════════════════════════════════════════
3515
- // Single-key shortcuts for common actions
3516
- function setupKeyboardShortcuts() {
3517
- if (process.stdin.isTTY) {
3518
- process.stdin.setRawMode(true);
3519
- process.stdin.resume();
3520
- process.stdin.setEncoding('utf8');
3521
-
3522
- // Premium styled keyboard shortcuts with gradient box
3523
- const accent = rgb(139, 92, 246);
3524
- const dim = c.dim;
3525
- const kw = 60;
3526
- console.log('');
3527
- console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
3528
- console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
3529
- console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
3530
- 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}`);
3531
- console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
3532
- console.log('');
3533
-
3534
- process.stdin.on('data', (key) => {
3535
- const k = key.toString().toLowerCase();
3536
-
3537
- // Ctrl+C or q = quit
3538
- if (k === '\u0003' || k === 'q') {
3539
- process.stdout.write(c.show);
3540
- console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
3541
- process.emit('SIGINT');
3542
- return;
3543
- }
3544
-
3545
- // p = pause all accounts
3546
- if (k === 'p') {
3547
- let count = 0;
3548
- workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
3549
- recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
3550
- scheduleRender();
3551
- return;
3552
- }
3553
-
3554
- // r = resume all accounts
3555
- if (k === 'r') {
3556
- let count = 0;
3557
- workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
3558
- recentLogs.push(`>> RESUMED ${count} accounts`);
3559
- scheduleRender();
3560
- return;
3561
- }
3562
-
3563
- // s = show status summary (pushed to log feed)
3564
- if (k === 's') {
3565
- const active = workers.filter(w => w.running && !w.paused).length;
3566
- const paused = workers.filter(w => w.paused).length;
3567
- const invalid = workers.filter(w => w._tokenInvalid).length;
3568
- const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
3569
- const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
3570
- const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
3571
- recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
3572
- recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
3573
- scheduleRender();
3574
- return;
3575
- }
3576
-
3577
- // ? or h = show help
3578
- if (k === '?' || k === 'h') {
3579
- recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
3580
- scheduleRender();
3581
- return;
3582
- }
3583
- });
3584
- }
3585
- }
3385
+ // Keyboard shortcuts removed no display to update
3586
3386
 
3587
3387
  // Export the start function for CLI
3588
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": "8.1.0",
3
+ "version": "8.10.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"