dankgrinder 8.1.0 → 8.2.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,
@@ -95,10 +94,20 @@ const c = {
95
94
  };
96
95
 
97
96
  const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
98
- // Unique marker written to stdout so we can query cursor position via DSR response
99
- const MARKER = '\x1b[6n\x1b[@@MARKER@@';
100
97
  const DANK_MEMER_ID = '270904126974590976';
101
98
 
99
+
100
+ // Simple uptime formatter
101
+ function formatUptime() {
102
+ const s = Math.floor((Date.now() - startTime) / 1000);
103
+ if (s < 60) return `${s}s`;
104
+ const m = Math.floor(s / 60);
105
+ if (m < 60) return `${m}m ${s % 60}s`;
106
+ const h = Math.floor(m / 60);
107
+ if (h < 24) return `${h}h ${m % 60}m`;
108
+ const d = Math.floor(h / 24);
109
+ return `${d}d ${h % 24}h`;
110
+ }
102
111
  // ── Safe options for search/crime ──────────────────────────
103
112
  // Object.freeze → V8 marks these as immutable, enabling inline caching
104
113
  // and preventing accidental mutation across 10K worker instances.
@@ -133,10 +142,25 @@ const CLUSTER_PREFIX = 'dkg:cluster:';
133
142
  function initRedis() {
134
143
  if (!redis && REDIS_URL) {
135
144
  try {
136
- redis = new Redis(REDIS_URL, { maxRetriesPerRequest: null, lazyConnect: true });
137
- redis.connect().catch(() => {});
145
+ redis = new Redis(REDIS_URL, {
146
+ maxRetriesPerRequest: 3,
147
+ retryStrategy: (times) => times > 5 ? null : Math.min(times * 500, 3000),
148
+ lazyConnect: true,
149
+ });
150
+ redis.connect().catch((e) => {
151
+ // Only warn once — don't spam on persistent connection failures
152
+ if (!redis || redis.status === 'wait') {
153
+ console.warn(`[Redis] connection failed: ${e.message} — continuing without Redis`);
154
+ }
155
+ });
156
+ redis.on('error', (e) => {
157
+ // Suppress common transient errors from spamming stderr
158
+ const msg = e?.message || '';
159
+ if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('connect')) return;
160
+ console.error(`[Redis] error: ${msg}`);
161
+ });
138
162
  } catch (e) {
139
- console.error('Redis connection failed', e);
163
+ // Redis optional continue without it
140
164
  }
141
165
  }
142
166
  }
@@ -258,17 +282,6 @@ function progressBar(value, max, width, filledColor, emptyColor) {
258
282
  return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
259
283
  }
260
284
 
261
- // ── Animated braille spinner frames ──────────────────────────
262
- const BRAILLE_SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
263
- const BLOCK_SPIN = ['▉', '▊', '▋', '▌', '▍', '▎', '▏', '▎', '▍', '▌', '▋', '▊'];
264
- const PULSE_CHARS = ['○', '◎', '●', '◉', '●', '◎'];
265
- function getSpinner(type = 'braille') {
266
- const now = Math.floor(Date.now() / 80);
267
- if (type === 'block') return BLOCK_SPIN[now % BLOCK_SPIN.length];
268
- if (type === 'pulse') return PULSE_CHARS[now % PULSE_CHARS.length];
269
- return BRAILLE_SPIN[now % BRAILLE_SPIN.length];
270
- }
271
-
272
285
  // ── Box drawing helpers ──────────────────────────────────────
273
286
  const BOX = {
274
287
  tl: '╭', tr: '╮', bl: '╰', br: '╯',
@@ -321,123 +334,17 @@ function colorBanner() {
321
334
  return out;
322
335
  }
323
336
 
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
-
337
+ // ── Simple Logging ─────────────────────────────────────────────
416
338
  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: '·',
339
+ const colorIcons = {
340
+ info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
341
+ error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
342
+ cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
343
+ buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
344
+ debug: `${c.dim}·${c.reset}`,
421
345
  };
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
- }
346
+ const tagCol = label ? `${label} ` : '';
347
+ console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
441
348
  }
442
349
 
443
350
  async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
@@ -869,7 +776,6 @@ class AccountWorker {
869
776
 
870
777
  setStatus(text) {
871
778
  this.lastStatus = stripAnsi(String(text || '')).replace(/\s+/g, ' ').trim();
872
- if (dashboardStarted) scheduleRender();
873
779
  }
874
780
 
875
781
  waitForDankMemer(timeout = 15000) {
@@ -1482,34 +1388,32 @@ class AccountWorker {
1482
1388
 
1483
1389
  // Update Redis with findings
1484
1390
  if (redis) {
1485
- if (currentLevel > 0) {
1486
- await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1487
- 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}`);
1391
+ try {
1392
+ if (currentLevel > 0) {
1393
+ await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1394
+ this._level = currentLevel;
1498
1395
  }
1499
- }
1396
+ if (lastLifesaverCount >= 0) {
1397
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
1398
+ this._lifesavers = lastLifesaverCount;
1399
+ if (lastLifesaverCount === 0) {
1400
+ await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1401
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1402
+ }
1403
+ }
1404
+ } catch { /* Redis errors non-fatal */ }
1500
1405
  }
1501
1406
 
1502
- return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1503
- } catch (e) {
1504
- lastError = e;
1505
- if (attempt < maxRetries - 1) {
1506
- await new Promise(r => setTimeout(r, delays[attempt]));
1507
- }
1407
+ return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1408
+ } catch (e) {
1409
+ lastError = e;
1410
+ if (attempt < maxRetries - 1) {
1411
+ await new Promise(r => setTimeout(r, delays[attempt]));
1508
1412
  }
1509
1413
  }
1510
- if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1511
- return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1512
1414
  }
1415
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1416
+ }
1513
1417
 
1514
1418
  // ── Run Single Command ──────────────────────────────────────
1515
1419
  // Each modular command handler sends the command, waits for response,
@@ -2844,10 +2748,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2844
2748
  REDIS_URL = process.env.REDIS_URL || '';
2845
2749
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2846
2750
 
2847
- process.stdout.write('\x1b[2J\x1b[H');
2848
- const tw = Math.min(process.stdout.columns || 80, 78);
2849
- const bar = c.dim + '─'.repeat(tw) + c.reset;
2850
-
2851
2751
  // Detect zlib-sync availability
2852
2752
  let hasZlib = false;
2853
2753
  try { require('zlib-sync'); hasZlib = true; } catch {}
@@ -2860,8 +2760,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2860
2760
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
2861
2761
  ` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
2862
2762
  );
2863
- console.log(bar);
2864
-
2865
2763
  log('info', `${c.dim}Fetching accounts...${c.reset}`);
2866
2764
 
2867
2765
  const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
@@ -2926,7 +2824,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2926
2824
 
2927
2825
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
2928
2826
  if (REDIS_URL) {
2929
- rawLogger.init(REDIS_URL).catch(() => {});
2827
+ rawLogger.init(redis);
2930
2828
  // Listen for DM events across all accounts — update worker state + dashboard LIVE
2931
2829
  rawLogger.onDmEvent((event, raw) => {
2932
2830
  const channelId = raw.channel_id;
@@ -2955,13 +2853,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2955
2853
  }
2956
2854
  }
2957
2855
  }
2958
- scheduleRender();
2959
2856
  }
2960
2857
 
2961
2858
  if (event.type === 'levelup') {
2962
2859
  if (event.to > 0) {
2963
2860
  w._level = event.to;
2964
- scheduleRender();
2965
2861
  }
2966
2862
  }
2967
2863
  }
@@ -2977,110 +2873,8 @@ async function start(apiKey, apiUrl, opts = {}) {
2977
2873
  console.log(` ${checks.join(' ')}`);
2978
2874
  console.log('');
2979
2875
 
2980
- // ── Phase 1: Login with per-account inline rendering ─────────────────────────
2981
- const startupTw = process.stdout.columns || 90;
2982
- const colNum = 4; // " #"
2983
- const colSts = 3; // "ST"
2984
- const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
2985
- const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
2986
- const colCmds = 8;
2987
- const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
2988
-
2989
- const loginStates = accounts.map((acc, i) => ({
2990
- name: acc.label || acc.id || '?',
2991
- done: false,
2992
- failed: false,
2993
- worker: null,
2994
- }));
2995
-
2996
- let loginLines = [];
2997
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2998
- for (let i = 0; i < loginStates.length; i++) {
2999
- const s = loginStates[i];
3000
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3001
- const name = s.name.substring(0, colName).padEnd(colName);
3002
- const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
3003
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3004
- loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
3005
- }
3006
- loginLines.push(` ${'─'.repeat(loginVis)}`);
3007
- for (const l of loginLines) console.log(l);
3008
-
3009
- // Dynamically capture the starting row of the login table via DSR.
3010
- // Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
3011
- // of the visible "@MARKER@@" text portion, which was causing the DSR
3012
- // response to be swallowed or delayed.
3013
- let loginBaseRow = 1;
3014
- const captureLoginRow = () => new Promise(resolve => {
3015
- const chunks = [];
3016
- const handler = (chunk) => {
3017
- chunks.push(chunk);
3018
- const raw = chunks.join('');
3019
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3020
- if (m) {
3021
- process.stdin.removeListener('data', handler);
3022
- loginBaseRow = parseInt(m[1], 10) + 1;
3023
- resolve();
3024
- }
3025
- };
3026
- process.stdin.on('data', handler);
3027
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3028
- process.stderr.write(MARKER);
3029
- setTimeout(resolve, 50);
3030
- });
3031
- await captureLoginRow();
3032
-
3033
- let loginPending = new Array(accounts.length).fill(true);
3034
- const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3035
-
3036
- const drawLoginSpinners = () => {
3037
- for (let i = 0; i < loginPending.length; i++) {
3038
- if (!loginPending[i]) continue;
3039
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3040
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3041
- const name = loginStates[i].name.substring(0, colName).padEnd(colName);
3042
- const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
3043
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3044
- const row = loginBaseRow + 1 + i; // +1 skips the top border line
3045
- moveToRow(row);
3046
- process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
3047
- }
3048
- // Move cursor back to bottom to avoid overwriting the bottom border
3049
- const lastRow = loginBaseRow + 1 + accounts.length + 1;
3050
- moveToRow(lastRow);
3051
- };
3052
- const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3053
-
3054
- const finalizeLoginLine = (idx, worker) => {
3055
- if (!loginPending[idx]) return;
3056
- loginPending[idx] = false;
3057
- const s = loginStates[idx];
3058
- s.done = true;
3059
- s.worker = worker;
3060
-
3061
- const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
3062
- const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3063
- let sts, guild, cmds;
3064
- if (worker._tokenInvalid) {
3065
- sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3066
- guild = 'INVALID'.padEnd(colGuild);
3067
- cmds = '···'.padEnd(colCmds);
3068
- s.failed = true;
3069
- } else if (worker.channel) {
3070
- sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3071
- const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
3072
- guild = gn.padEnd(colGuild);
3073
- cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
3074
- } else {
3075
- sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
3076
- guild = 'timeout'.padEnd(colGuild);
3077
- cmds = '···'.padEnd(colCmds);
3078
- }
3079
- const row = loginBaseRow + 1 + idx; // +1 skips the top border line
3080
- moveToRow(row);
3081
- process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3082
- };
3083
-
2876
+ // ── Phase 1: Login accounts ─────────────────────────────────────────
2877
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
3084
2878
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
3085
2879
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
3086
2880
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
@@ -3096,203 +2890,58 @@ async function start(apiKey, apiUrl, opts = {}) {
3096
2890
  const worker = new AccountWorker(acc, i + idx);
3097
2891
  workers.push(worker);
3098
2892
  workerMap.set(acc.id, worker);
3099
- loginStates[i + idx].worker = worker;
3100
2893
  await worker.start();
3101
- finalizeLoginLine(i + idx, worker);
3102
2894
  }));
3103
2895
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3104
2896
  hintGC();
3105
2897
  }
3106
2898
 
3107
- clearInterval(loginSpinnerInterval);
3108
2899
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3109
2900
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3110
2901
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3111
- console.log(`\r\x1b[2K ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3112
- console.log('');
2902
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${loginDone}/${accounts.length} accounts connected`);
3113
2903
  if (invalidWorkers.length > 0) {
3114
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3115
- for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token is invalid or expired`);
3116
- console.log('');
2904
+ log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
2905
+ for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
3117
2906
  }
3118
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2907
+ if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login`);
3119
2908
 
3120
2909
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3121
2910
 
3122
- // ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
3123
- const iColNum = 4;
3124
- const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3125
- const iColItems = 8;
3126
- const iColVal = 16;
3127
- const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3128
-
3129
- // Print a unique marker, query its position, then overwrite it with the table
3130
- // Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
3131
- let invBaseRow = 1;
3132
- const captureRow = () => new Promise(resolve => {
3133
- const chunks = [];
3134
- const handler = (chunk) => {
3135
- chunks.push(chunk);
3136
- const raw = chunks.join('');
3137
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3138
- if (m) {
3139
- process.stdin.removeListener('data', handler);
3140
- invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
3141
- resolve();
3142
- }
3143
- };
3144
- process.stdin.on('data', handler);
3145
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3146
- process.stderr.write(MARKER);
3147
- setTimeout(resolve, 50);
3148
- });
3149
- await captureRow();
3150
-
3151
- // Now print the inventory table starting at invBaseRow
3152
- const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3153
- console.log(` ${'─'.repeat(invVis)}`);
3154
- for (let i = 0; i < activeWorkers.length; i++) {
3155
- const w = activeWorkers[i];
3156
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3157
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3158
- console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'···'.padEnd(iColVal)}${c.reset}`);
3159
- }
3160
- console.log(` ${'─'.repeat(invVis)}`);
3161
-
3162
- let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3163
- const drawInvProgress = () => {
3164
- if (invPending === 0) return;
3165
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3166
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3167
- const barW = Math.min(20, startupTw - 40);
3168
- const filled = Math.round(pct * barW);
3169
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3170
- const pctStr = `${Math.round(pct * 100)}%`;
3171
- invMoveToRow(invBaseRow);
3172
- process.stdout.write(` ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - invPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}${pctStr}${c.reset} \x1b[K`);
3173
- };
3174
- const invSpinnerInterval = setInterval(drawInvProgress, 80);
3175
-
3176
- await Promise.all(activeWorkers.map(async (w, i) => {
3177
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3178
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3179
- let invRes;
3180
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3181
- catch { invRes = { ok: false }; }
3182
- invPending--;
3183
- const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3184
- const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3185
- const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3186
- const itemStr = `${items}`.padEnd(iColItems);
3187
- const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3188
- const row = invBaseRow + 1 + i;
3189
- invMoveToRow(row);
3190
- process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3191
- if (invRes?.ok) invDone++; else invFailed++;
2911
+ // ── Phase 2: Inventory check ─────────────────────────────────────
2912
+ console.log(` ${rgb(34, 211, 238)}·${c.reset} Checking inventory (${activeWorkers.length} accounts)...`);
2913
+ let invDone = 0, invFailed = 0;
2914
+ await Promise.all(activeWorkers.map(async (w) => {
2915
+ try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2916
+ catch { invFailed++; return; }
2917
+ invDone++;
3192
2918
  }));
3193
2919
 
3194
- clearInterval(invSpinnerInterval);
3195
- process.stdout.write(`\r\x1b[2K`);
3196
-
3197
2920
  if (invFailed > 0) {
3198
- console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}Inventory incomplete${c.reset} ${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
3199
- log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
2921
+ console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed not starting grind loops`);
3200
2922
  return;
3201
2923
  }
3202
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${activeWorkers.length}${c.reset} ${c.dim}all clear${c.reset}`);
3203
- console.log('');
2924
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Inventory: ${invDone}/${activeWorkers.length} clear`);
3204
2925
 
3205
- // ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
3206
- const bColNum = 4;
3207
- const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3208
- const bColWallet = 12;
3209
- const bColBank = 12;
3210
- const bColTotal = 14;
3211
- const bColLs = 4;
3212
- const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3213
-
3214
- // Capture starting row for balance phase
3215
- // Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
3216
- let balBaseRow = 1;
3217
- const balCaptureRow = () => new Promise(resolve => {
3218
- const chunks = [];
3219
- const handler = (chunk) => {
3220
- chunks.push(chunk);
3221
- const raw = chunks.join('');
3222
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3223
- if (m) {
3224
- process.stdin.removeListener('data', handler);
3225
- balBaseRow = parseInt(m[1], 10) + 1;
3226
- resolve();
3227
- }
3228
- };
3229
- process.stdin.on('data', handler);
3230
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3231
- process.stderr.write(MARKER);
3232
- setTimeout(resolve, 50);
3233
- });
3234
- await balCaptureRow();
3235
-
3236
- const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3237
- console.log(` ${'─'.repeat(balVis)}`);
3238
- for (let i = 0; i < activeWorkers.length; i++) {
3239
- const w = activeWorkers[i];
3240
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3241
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3242
- console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'···'.padEnd(bColBank)}${c.reset} ${c.dim}${'···'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3243
- }
3244
- console.log(` ${'─'.repeat(balVis)}`);
3245
-
3246
- let balDone = 0, balPending = activeWorkers.length;
3247
- const drawBalProgress = () => {
3248
- if (balPending === 0) return;
3249
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3250
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3251
- const barW = Math.min(20, startupTw - 40);
3252
- const filled = Math.round(pct * barW);
3253
- const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3254
- balMoveToRow(balBaseRow);
3255
- process.stdout.write(` ${rgb(251, 191, 36)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - balPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} \x1b[K`);
3256
- };
3257
- const balSpinnerInterval = setInterval(drawBalProgress, 80);
3258
-
3259
- await Promise.all(activeWorkers.map(async (w, i) => {
2926
+ // ── Phase 2.5: Balance check ────────────────────────────────────
2927
+ console.log(` ${rgb(251, 191, 36)}·${c.reset} Checking balance (${activeWorkers.length} accounts)...`);
2928
+ await Promise.all(activeWorkers.map(async (w) => {
3260
2929
  try { await w.checkBalance(true); } catch {}
3261
- balPending--;
3262
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3263
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3264
- const wallet = w.stats?.balance || 0;
3265
- const bank = w.stats?.bankBalance || 0;
3266
- const ls = w._lifesavers ?? '?';
3267
- const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3268
- const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3269
- const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3270
- const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3271
- const row = balBaseRow + 1 + i;
3272
- balMoveToRow(row);
3273
- process.stdout.write(` ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}\x1b[K`);
3274
- balDone++;
3275
2930
  }));
3276
2931
 
3277
- clearInterval(balSpinnerInterval);
3278
- process.stdout.write(`\r\x1b[2K`);
3279
-
3280
2932
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3281
2933
  for (const w of activeWorkers) {
3282
2934
  totalWallet += w.stats?.balance || 0;
3283
2935
  totalBank += w.stats?.bankBalance || 0;
3284
2936
  if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
3285
2937
  }
3286
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Balance${c.reset} Total: ${c.bold}${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
2938
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Balance: ${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
3287
2939
  if (noLifesaverAccounts.length > 0) {
3288
- console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}WARNING: ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS!${c.reset} Crime/Search disabled for: ${noLifesaverAccounts.join(', ')}`);
2940
+ console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
3289
2941
  }
3290
- console.log('');
3291
-
3292
2942
 
3293
- // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3294
- const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3295
- console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
2943
+ // ── Phase 2.75: DM history check ────────────────────────────────
2944
+ console.log(` ${rgb(139, 92, 246)}·${c.reset} Checking DM history...`);
3296
2945
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
3297
2946
  for (const w of activeWorkers) {
3298
2947
  try {
@@ -3301,28 +2950,12 @@ async function start(apiKey, apiUrl, opts = {}) {
3301
2950
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3302
2951
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
3303
2952
  if (dm.lifesavers === -1) dmUnknown.push(w.username);
3304
- // Store level and lifesaver for dashboard
3305
2953
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
3306
2954
  if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
3307
- const parts = [];
3308
- if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
3309
- if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
3310
- if (dm.lifesavers >= 0) {
3311
- const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3312
- parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
3313
- } else {
3314
- // Unknown lifesavers — pulse to show pending
3315
- const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3316
- parts.push(`${D}${pulse}♥?${c.reset}`);
3317
- }
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
2955
  } catch {}
3322
2956
  }
3323
2957
  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' });
3325
- // Set Redis keys to block crime/search
2958
+ log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3326
2959
  for (const w of activeWorkers) {
3327
2960
  if (dmNoLs.includes(w.username) && redis) {
3328
2961
  try {
@@ -3333,45 +2966,23 @@ async function start(apiKey, apiUrl, opts = {}) {
3333
2966
  }
3334
2967
  }
3335
2968
  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' });
3337
- // Crime/search on these accounts will be skipped via safety hold until the live
3338
- // DM gateway listener detects a death (→ sets count) or confirms clean.
2969
+ log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
3339
2970
  }
3340
2971
  const dmSummaryParts = [];
3341
2972
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3342
2973
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3343
2974
  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' });
3345
- console.log('');
3346
-
3347
- console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
3348
-
3349
- // DEBUG: activeWorkers confirmed
2975
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
3350
2976
 
2977
+ // ── Phase 3: Start grind loops ───────────────────────────────────
2978
+ console.log(` ${rgb(139, 92, 246)}>>>${c.reset} Starting grind loops...`);
3351
2979
  // Phase 3: Start all grind loops (only for valid workers)
3352
2980
  for (const w of activeWorkers) {
3353
2981
  if (!shutdownCalled) w.grindLoop();
3354
2982
  }
3355
2983
 
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();
2984
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
2985
+ console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
3375
2986
 
3376
2987
  // Cluster heartbeat — lets other nodes see this node is alive
3377
2988
  if (CLUSTER_ENABLED) {
@@ -3428,7 +3039,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3428
3039
  const before = workers.length;
3429
3040
  // Keep ALL workers visible — never remove from array (user wants to see gaps)
3430
3041
  // Only clean up workerMap entries for accounts fully removed from API
3431
- if (workers.length !== before) scheduleRender();
3042
+ if (workers.length !== before) { /* workers changed */ }
3432
3043
  } catch {}
3433
3044
  }, 10_000);
3434
3045
 
@@ -3437,18 +3048,9 @@ async function start(apiKey, apiUrl, opts = {}) {
3437
3048
  if (sigintHandled) return;
3438
3049
  sigintHandled = true;
3439
3050
  shutdownCalled = true;
3440
- dashboardStarted = false;
3441
3051
  setDashboardActive(false);
3442
3052
  process.stdout.write(c.show);
3443
3053
 
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
3054
  const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3453
3055
  console.log('');
3454
3056
  console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
@@ -3510,79 +3112,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3510
3112
  }
3511
3113
 
3512
3114
  // ══════════════════════════════════════════════════════════════
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
- }
3115
+ // Keyboard shortcuts removed no display to update
3586
3116
 
3587
3117
  // Export the start function for CLI
3588
3118
  module.exports = { start };
package/lib/rawLogger.js CHANGED
@@ -30,7 +30,14 @@ const memRing = [];
30
30
  let memIdx = 0;
31
31
 
32
32
  // ── Redis init ──
33
- async function init(redisUrl) {
33
+ async function init(redisUrlOrInstance) {
34
+ // Support passing an existing Redis instance directly (preferred)
35
+ if (redisUrlOrInstance && typeof redisUrlOrInstance.status !== 'undefined') {
36
+ redis = redisUrlOrInstance;
37
+ redisReady = redis.status === 'ready';
38
+ return;
39
+ }
40
+ const redisUrl = redisUrlOrInstance;
34
41
  if (!redisUrl) {
35
42
  console.log('[rawLogger] No Redis URL — raw logging disabled');
36
43
  return;
@@ -54,7 +61,10 @@ async function init(redisUrl) {
54
61
  redisReady = true;
55
62
  console.log('[rawLogger] Redis connected');
56
63
  redis.on('error', (e) => {
57
- console.error(`[rawLogger] Redis error: ${e.message}`);
64
+ // Suppress common transient network errors from spamming stderr
65
+ const msg = e?.message || '';
66
+ if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('read') || msg.includes('connect')) return;
67
+ console.error(`[rawLogger] Redis error: ${msg}`);
58
68
  redisReady = false;
59
69
  });
60
70
  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.2.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"