dankgrinder 6.0.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/grinder.js +302 -134
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -219,6 +219,23 @@ function gradientLine(text, from, to) {
219
219
  return out + c.reset;
220
220
  }
221
221
 
222
+ // ── Sparkline graph for earnings trend ───────────────────────
223
+ const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
224
+ function drawSparkline(data, width = 12) {
225
+ if (!data || data.length === 0) return c.dim + '──────' + c.reset;
226
+ const recent = data.slice(-width);
227
+ const min = Math.min(...recent);
228
+ const max = Math.max(...recent);
229
+ const range = max - min || 1;
230
+ return recent.map(v => {
231
+ const idx = Math.min(7, Math.floor(((v - min) / range) * 8));
232
+ const char = SPARK_CHARS[idx] || '▁';
233
+ return v >= max * 0.9 ? rgb(52, 211, 153) + char + c.reset :
234
+ v <= min * 1.1 ? rgb(239, 68, 68) + char + c.reset :
235
+ rgb(139, 92, 246) + char + c.reset;
236
+ }).join('');
237
+ }
238
+
222
239
  const BANNER_RAW = [
223
240
  ' ██████╗ █████╗ ███╗ ██╗██╗ ██╗',
224
241
  ' ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝',
@@ -264,10 +281,15 @@ let totalCoins = 0;
264
281
  let totalCommands = 0;
265
282
  let startTime = Date.now();
266
283
  let shutdownCalled = false;
284
+ let sessionPeakCoins = 0;
285
+ let isNewHigh = false;
267
286
  // RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
268
287
  const recentLogs = new RingBuffer(6);
269
288
  const MAX_LOGS = 6;
270
289
  const RENDER_THROTTLE_MS = 250;
290
+ // Earnings history for sparkline (sample every 10 seconds)
291
+ const earningsHistory = new RingBuffer(20);
292
+ let lastEarningsSample = 0;
271
293
 
272
294
  function formatUptime() {
273
295
  const s = Math.floor((Date.now() - startTime) / 1000);
@@ -311,70 +333,109 @@ function renderDashboard() {
311
333
  totalErrors += w.stats.errors || 0;
312
334
  }
313
335
  const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
336
+ // Track session peak and new high
337
+ if (totalCoins > sessionPeakCoins) {
338
+ sessionPeakCoins = totalCoins;
339
+ isNewHigh = true;
340
+ setTimeout(() => { isNewHigh = false; }, 3000);
341
+ }
314
342
 
315
343
  const lines = [];
316
344
  const tw = Math.min(process.stdout.columns || 80, 78);
317
345
  const thinBar = c.dim + '─'.repeat(tw) + c.reset;
318
346
  const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
319
347
  const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
348
+ const doubleBarMid = rgb(139, 92, 246) + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
320
349
  const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
321
350
 
322
- // Header with dynamic version, command count, and status
351
+ // Header with title, stats, and mode
323
352
  lines.push(doubleBar);
324
353
  const cmdCount = AccountWorker.COMMAND_MAP.length;
325
354
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
326
355
  const mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
327
356
 
328
- // Animated spinner based on time
357
+ // Animated spinner and gradient title
329
358
  const spinners = ['◐', '◓', '◑', '◒'];
330
359
  const spinner = spinners[Math.floor(Date.now() / 250) % 4];
331
360
  const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
332
-
333
- lines.push(
334
- ` ${rgb(139, 92, 246)}${c.bold}DankGrinder${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}` +
335
- ` ${c.dim}·${c.reset} ${c.white}${cmdCount} Cmds${c.reset}` +
336
- ` ${c.dim}·${c.reset} ${mode}` +
337
- ` ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
338
- );
339
-
340
- // Stats row with enhanced visual indicators
361
+ const gradientTitle = gradientLine('DankGrinder', [192, 132, 252], [34, 211, 238]);
362
+
363
+ // Network quality indicator (based on recent latency/errors)
364
+ const netQuality = workers.length > 0
365
+ ? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length
366
+ : 1;
367
+ const netIcon = netQuality > 0.8 ? `${rgb(52, 211, 153)}◉${c.reset}` :
368
+ netQuality > 0.5 ? `${rgb(251, 191, 36)}◉${c.reset}` :
369
+ `${rgb(239, 68, 68)}◉${c.reset}`;
370
+ const netLabel = netQuality > 0.8 ? `${c.dim}good${c.reset}` :
371
+ netQuality > 0.5 ? `${c.dim}fair${c.reset}` :
372
+ `${c.dim}poor${c.reset}`;
373
+
374
+ lines.push(` ${c.bold}${gradientTitle}${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}`);
375
+ lines.push(` ${mode} ${c.dim}·${c.reset} ${netIcon} ${netLabel} ${c.dim}·${c.reset} ${c.white}${cmdCount} commands${c.reset} ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}live${c.reset}`);
376
+
377
+ // Stats row 1: Balance & Earnings
378
+ lines.push(doubleBarMid);
341
379
  const liveIcon = rgb(52, 211, 153) + '●' + c.reset;
342
380
  const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
343
381
  const earnStr = `${rgb(52, 211, 153)}▲ ${formatCoins(totalCoins)}${c.reset}`;
344
- // Coins/hour rate
345
382
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
346
383
  const coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
347
384
  const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
385
+
386
+ // Stats row 2: Commands & Performance
348
387
  const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
349
388
  const rateStr = successRate >= 95
350
389
  ? `${rgb(52, 211, 153)}● ${successRate}%${c.reset}`
351
390
  : successRate >= 80 ? `${rgb(251, 191, 36)}● ${successRate}%${c.reset}`
352
391
  : `${rgb(239, 68, 68)}● ${successRate}%${c.reset}`;
392
+ const cpmVal = globalCmdRate.getRate().toFixed(1);
393
+ const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
394
+
395
+ // Stats row 3: Uptime & Memory
353
396
  const upStr = `${rgb(251, 191, 36)}◷ ${formatUptime()}${c.reset}`;
354
- // Memory usage (RSS in MB) with bar indicator
355
397
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
356
398
  const memPct = Math.min(100, (memMB / 1024) * 100);
357
399
  const memBarWidth = Math.floor((memPct / 100) * 10);
358
400
  const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
359
401
  const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
360
402
  const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
361
- // Commands/minute from SlidingWindowCounter
362
- const cpmVal = globalCmdRate.getRate().toFixed(1);
363
- const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
364
- lines.push(
365
- ` ${liveIcon} ${balStr} ${c.dim}│${c.reset} ${earnStr} ${c.dim}(${c.reset}${rateLabel}${c.dim})${c.reset} ${c.dim}│${c.reset} ${cmdStr} ${c.dim}(${c.reset}${rateStr}${c.dim})${c.reset} ${c.dim}│${c.reset} ${cpmStr} ${c.dim}│${c.reset} ${upStr} ${c.dim}│${c.reset} ${memStr}`
366
- );
403
+
404
+ // Sample earnings for sparkline every 10 seconds
405
+ const now = Date.now();
406
+ if (now - lastEarningsSample > 10000) {
407
+ earningsHistory.push(totalCoins);
408
+ lastEarningsSample = now;
409
+ }
410
+ const sparkline = drawSparkline(earningsHistory.toArray(), 16);
411
+ const peakIndicator = isNewHigh ? `${rgb(255, 100, 100)} 🚀 NEW HIGH!${c.reset}` : '';
412
+
413
+ lines.push(` ${liveIcon} ${balStr} ${c.dim}│${c.reset} ${earnStr} ${c.dim}(${c.reset}${rateLabel}${c.dim})${c.reset}${peakIndicator}`);
414
+ lines.push(` ${cmdStr} ${c.dim}(${c.reset}${rateStr}${c.dim})${c.reset} ${c.dim}│${c.reset} ${cpmStr} ${c.dim}│${c.reset} ${upStr}`);
415
+ lines.push(` ${c.dim}Peak:${c.reset} ${rgb(255, 215, 0)}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${c.dim}│${c.reset} Trend: ${sparkline} ${c.dim}│${c.reset} ${memStr}`);
367
416
  lines.push(thinBar);
368
417
 
369
418
  // Worker rows — paginated for 10K+ accounts
370
419
  // Renders up to MAX_VISIBLE_WORKERS rows individually, then shows
371
420
  // a compact summary for the rest. This keeps terminal responsive.
372
- const MAX_VISIBLE_WORKERS = 25;
373
- const nameWidth = Math.min(16, tw > 65 ? 16 : 10);
374
- const statusWidth = Math.max(16, tw - nameWidth - 34);
421
+ const MAX_VISIBLE_WORKERS = 30;
422
+ const nameWidth = Math.min(18, tw > 70 ? 18 : 12);
423
+ const statusWidth = Math.max(18, tw - nameWidth - 42);
375
424
  const RE_ANSI_STRIP = /\x1b\[[0-9;]*m/g;
376
425
 
377
- const renderWorkerRow = (wk) => {
426
+ // Find top 3 earners for highlighting
427
+ const topEarners = [...workers]
428
+ .filter(w => w.running && (w.stats.coins || 0) > 0)
429
+ .sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
430
+ .slice(0, 3);
431
+ const topEarnerIds = new Set(topEarners.map(w => w.account.id));
432
+
433
+ // Column headers
434
+ lines.push(` ${c.dim}#${c.reset} ${c.dim}Account${' '.repeat(nameWidth - 5)}${c.reset} ${c.dim}Balance${c.reset} ${c.dim}Earned${c.reset} ${c.dim}Status${c.reset}`);
435
+ lines.push(` ${c.dim}${'─'.repeat(tw - 4)}${c.reset}`);
436
+
437
+ const renderWorkerRow = (wk, index) => {
438
+ const pos = `${c.dim}#${c.reset}${c.bold}${(index + 1).toString().padStart(2, ' ')}${c.reset}`;
378
439
  const rawStatus = (wk.lastStatus || 'idle').replace(RE_ANSI_STRIP, '');
379
440
  const last = rawStatus.substring(0, statusWidth);
380
441
 
@@ -401,12 +462,19 @@ function renderDashboard() {
401
462
  stateLabel = `${c.dim}${last}${c.reset}`;
402
463
  }
403
464
 
465
+ // Top earner medal
466
+ let medal = ' ';
467
+ if (topEarnerIds.has(wk.account.id)) {
468
+ const rank = topEarners.findIndex(e => e.account.id === wk.account.id);
469
+ medal = rank === 0 ? `${rgb(255, 215, 0)}🥇${c.reset}` : rank === 1 ? `${rgb(192, 192, 192)}🥈${c.reset}` : `${rgb(205, 127, 50)}🥉${c.reset}`;
470
+ }
471
+
404
472
  const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameWidth)}${c.reset}`;
405
473
  const bal = wk.stats.balance > 0
406
- ? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
407
- : `${c.dim}⏣ -${c.reset}`;
474
+ ? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(8)}${c.reset}`
475
+ : `${c.dim}⏣ -${c.reset}`;
408
476
 
409
- // Mini progress bar for earned coins (visual indicator of activity)
477
+ // Enhanced progress bar for earned coins
410
478
  const earnedNum = wk.stats.coins || 0;
411
479
  const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
412
480
  const earnedBar = earnedNum > 0
@@ -416,14 +484,14 @@ function renderDashboard() {
416
484
  ? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
417
485
  : `${c.dim}+0${c.reset} ${earnedBar}`;
418
486
 
419
- lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
487
+ lines.push(` ${pos} ${medal}${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length + 2)} ${bal} ${earned} ${stateLabel}`);
420
488
  };
421
489
 
422
490
  if (workers.length <= MAX_VISIBLE_WORKERS) {
423
- for (let i = 0; i < workers.length; i++) renderWorkerRow(workers[i]);
491
+ for (let i = 0; i < workers.length; i++) renderWorkerRow(workers[i], i);
424
492
  } else {
425
493
  // Pagination: show first MAX_VISIBLE_WORKERS, summarize rest
426
- for (let i = 0; i < MAX_VISIBLE_WORKERS; i++) renderWorkerRow(workers[i]);
494
+ for (let i = 0; i < MAX_VISIBLE_WORKERS; i++) renderWorkerRow(workers[i], i);
427
495
  const remaining = workers.length - MAX_VISIBLE_WORKERS;
428
496
  let hiddenActive = 0, hiddenPaused = 0, hiddenRecovering = 0, hiddenOffline = 0;
429
497
  for (let i = MAX_VISIBLE_WORKERS; i < workers.length; i++) {
@@ -433,31 +501,33 @@ function renderDashboard() {
433
501
  else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hiddenRecovering++;
434
502
  else hiddenActive++;
435
503
  }
436
- const parts = [`${c.dim}... +${remaining} more${c.reset}`];
437
- if (hiddenActive > 0) parts.push(`${rgb(52, 211, 153)}${hiddenActive} active${c.reset}`);
438
- if (hiddenPaused > 0) parts.push(`${rgb(251, 191, 36)}${hiddenPaused} paused${c.reset}`);
439
- if (hiddenRecovering > 0) parts.push(`${rgb(251, 191, 36)}${hiddenRecovering} recovering${c.reset}`);
440
- if (hiddenOffline > 0) parts.push(`${c.dim}${hiddenOffline} offline${c.reset}`);
504
+ const parts = [`${c.dim}┃${c.reset} ${c.dim}... +${remaining} more${c.reset}`];
505
+ if (hiddenActive > 0) parts.push(`${rgb(52, 211, 153)}${hiddenActive} active${c.reset}`);
506
+ if (hiddenPaused > 0) parts.push(`${rgb(251, 191, 36)}${hiddenPaused} paused${c.reset}`);
507
+ if (hiddenRecovering > 0) parts.push(`${rgb(251, 191, 36)}${hiddenRecovering} recovering${c.reset}`);
508
+ if (hiddenOffline > 0) parts.push(`${c.dim}${hiddenOffline} offline${c.reset}`);
441
509
  lines.push(` ${parts.join(` ${c.dim}·${c.reset} `)}`);
442
510
  }
443
511
 
444
- // Recovery summary line
512
+ // Recovery & status summary line with enhanced styling
445
513
  const recoveringCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
446
514
  const pausedCount = workers.filter(w => w.paused).length;
447
515
  const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
448
- if (recoveringCount > 0 || pausedCount > 0 || totalRecoveries > 0) {
516
+ const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
517
+ if (recoveringCount > 0 || pausedCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
449
518
  const parts = [];
450
519
  if (recoveringCount > 0) parts.push(`${rgb(251, 191, 36)}↻ ${recoveringCount} recovering${c.reset}`);
451
520
  if (pausedCount > 0) parts.push(`${rgb(239, 68, 68)}⏸ ${pausedCount} paused${c.reset}`);
452
521
  if (totalRecoveries > 0) parts.push(`${c.dim}${totalRecoveries} auto-recovered${c.reset}`);
453
- lines.push(` ${parts.join(` ${c.dim}·${c.reset} `)}`);
522
+ if (totalDisconnects > 0) parts.push(`${c.dim}${totalDisconnects} reconnects${c.reset}`);
523
+ lines.push(` ${c.dim}┃${c.reset} ${parts.join(` ${c.dim}·${c.reset} `)}`);
454
524
  }
455
525
 
456
- // Cluster info line
526
+ // Cluster info line with enhanced styling
457
527
  if (CLUSTER_ENABLED) {
458
528
  const nodeShort = NODE_ID.substring(0, 12);
459
529
  const claimedCount = workers.filter(w => w.running).length;
460
- lines.push(` ${rgb(34, 211, 238)}⊞${c.reset} ${c.dim}Node: ${nodeShort} · ${claimedCount} claimed${c.reset}`);
530
+ lines.push(` ${rgb(34, 211, 238)}╔${c.reset}${c.dim} Cluster: ${nodeShort} · ${claimedCount} claimed ${c.reset}${rgb(34, 211, 238)}╗${c.reset}`);
461
531
  }
462
532
 
463
533
  // Log section
@@ -1170,11 +1240,10 @@ class AccountWorker {
1170
1240
  const tries = Math.max(1, Number.isFinite(maxAttempts) ? maxAttempts : 1);
1171
1241
  let lastErr = null;
1172
1242
  for (let attempt = 1; attempt <= tries; attempt++) {
1173
- if (startupProgress && Number.isInteger(startupProgress.current) && Number.isInteger(startupProgress.total)) {
1174
- this.log('info', `Checking inventory... (${startupProgress.current}/${startupProgress.total}) [try ${attempt}/${tries}]`);
1175
- } else {
1176
- this.log('info', `Checking inventory...${tries > 1 ? ` [try ${attempt}/${tries}]` : ''}`);
1177
- }
1243
+ const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1244
+ const attemptLabel = tries > 1 ? ` [try ${attempt}/${tries}]` : '';
1245
+ const progressLine = `${baseLabel}${c.bold} ${this.username}${c.reset}${attemptLabel}`;
1246
+ process.stdout.write(`\x1b[2K\r${progressLine}`);
1178
1247
 
1179
1248
  try {
1180
1249
  const result = await commands.runInventory({
@@ -1184,9 +1253,9 @@ class AccountWorker {
1184
1253
  accountId: this.account.id,
1185
1254
  redis,
1186
1255
  onPageProgress: ({ page, total }) => {
1187
- // Minimal progress update on same line
1256
+ // Update progress on same line
1188
1257
  const erase = '\x1b[2K\r';
1189
- process.stdout.write(`${erase}${this.color}[inv] ${page}/${total}${c.reset}`);
1258
+ process.stdout.write(`${erase}${baseLabel} ${c.bold}${this.username}${c.reset} · page ${page}/${total}${attemptLabel}`);
1190
1259
  },
1191
1260
  });
1192
1261
 
@@ -1194,9 +1263,9 @@ class AccountWorker {
1194
1263
  throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
1195
1264
  }
1196
1265
 
1197
- // Add newline after inventory progress
1198
- process.stdout.write('\n');
1199
- this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1266
+ // Final result on same line
1267
+ const resultLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.green}${result.items?.length || 0} items${c.reset}, ⏣ ${c.green}${(result.totalValue || 0).toLocaleString()}${c.reset} net${attemptLabel}`;
1268
+ process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1200
1269
  try {
1201
1270
  await fetch(`${API_URL}/api/grinder/inventory`, {
1202
1271
  method: 'POST',
@@ -1220,7 +1289,9 @@ class AccountWorker {
1220
1289
  } catch (e) {
1221
1290
  lastErr = e;
1222
1291
  if (attempt < tries) {
1223
- this.log('warn', `Inventory attempt ${attempt}/${tries} failed (${e.message}). Retrying...`);
1292
+ const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1293
+ const retryLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.yellow}attempt ${attempt}/${tries} failed${c.reset} — retrying...`;
1294
+ process.stdout.write(`\x1b[2K\r${retryLine}\n`);
1224
1295
  await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
1225
1296
  continue;
1226
1297
  }
@@ -1229,7 +1300,9 @@ class AccountWorker {
1229
1300
 
1230
1301
  throw lastErr || new Error('inventory check failed');
1231
1302
  } catch (e) {
1232
- this.log('error', `Inventory check failed: ${e.message}`);
1303
+ const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1304
+ const failLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.red}failed${c.reset} — ${e.message}`;
1305
+ process.stdout.write(`\x1b[2K\r${failLine}\n`);
1233
1306
  return { ok: false, error: e.message };
1234
1307
  } finally {
1235
1308
  this._invRunning = false;
@@ -2114,7 +2187,7 @@ class AccountWorker {
2114
2187
  }
2115
2188
 
2116
2189
  const prefix = this.account.use_slash ? '/' : 'pls';
2117
- this.setStatus(`pls ${item.cmd}`);
2190
+ this.setStatus(formatCommandName(item.cmd));
2118
2191
 
2119
2192
  // Report "running" to dashboard
2120
2193
  const nextItemRun = this.commandQueue?.peek?.();
@@ -2226,7 +2299,7 @@ class AccountWorker {
2226
2299
  if (text.includes('alert') || text.includes('notification') ||
2227
2300
  text.includes('you have a pending') || text.includes('check your alerts')) {
2228
2301
  if (!this.busy) {
2229
- this.log('info', 'Alert detected → running pls alert');
2302
+ this.log('info', 'Alert detected → running alert');
2230
2303
  this.busy = true;
2231
2304
  const prefix = this.account.use_slash ? '/' : 'pls';
2232
2305
  this.runCommand('alert', prefix).finally(() => { this.busy = false; });
@@ -2327,22 +2400,37 @@ class AccountWorker {
2327
2400
  if (!this.account.discord_token) { this.log('error', 'No token'); return; }
2328
2401
  if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
2329
2402
 
2403
+ const LOGIN_TIMEOUT_MS = 30_000; // 30s max per account login
2404
+
2330
2405
  return new Promise((resolve) => {
2406
+ let resolved = false;
2407
+ const done = () => { if (!resolved) { resolved = true; resolve(); } };
2408
+
2409
+ // Timeout guard — don't let a single account hang the batch
2410
+ const timeoutId = setTimeout(() => {
2411
+ if (!resolved) {
2412
+ this.log('warn', 'Login timed out after 30s — will retry in background');
2413
+ done();
2414
+ // Retry login in background after timeout
2415
+ this._retryLoginBackground();
2416
+ }
2417
+ }, LOGIN_TIMEOUT_MS);
2418
+
2331
2419
  this.client.on('ready', async () => {
2420
+ clearTimeout(timeoutId);
2332
2421
  this.username = this.client.user.tag || this.username;
2333
2422
  this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
2334
- try {
2335
- await fetch(`${API_URL}/api/grinder/status`, {
2336
- method: 'POST',
2337
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2338
- body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl }),
2339
- });
2340
- } catch { /* silent */ }
2423
+ // Report status non-blocking
2424
+ fetch(`${API_URL}/api/grinder/status`, {
2425
+ method: 'POST',
2426
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2427
+ body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl }),
2428
+ }).catch(() => {});
2341
2429
 
2342
2430
  this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2343
2431
  if (!this.channel) {
2344
2432
  this.log('error', `Channel not found`);
2345
- resolve(); return;
2433
+ done(); return;
2346
2434
  }
2347
2435
 
2348
2436
  const enabledCmds = [
@@ -2365,49 +2453,108 @@ class AccountWorker {
2365
2453
  this.log('success', `#${chName} · ${enabledCmds.length} cmds`);
2366
2454
  this.setStatus('starting...');
2367
2455
 
2368
- // Load daily/weekly/monthly done state from Redis
2369
- if (redis) {
2370
- for (const cmd of ['daily', 'weekly', 'monthly', 'drops']) {
2371
- try {
2372
- const val = await redis.get(`dkg:done:${this.account.id}:${cmd}`);
2373
- if (val) {
2374
- const ttlSec = await redis.ttl(`dkg:done:${this.account.id}:${cmd}`);
2375
- if (ttlSec > 0) {
2376
- this.doneToday.set(cmd, Date.now() + ttlSec * 1000);
2377
- this.log('info', `${cmd} already claimed (${Math.round(ttlSec / 3600)}h remaining)`);
2378
- }
2379
- }
2380
- } catch {}
2456
+ // Load daily/weekly/monthly done state from Redis (non-blocking for login speed)
2457
+ this._loadRedisState().catch(() => {});
2458
+
2459
+ // Reduced settle time — 200ms is enough for most gateways
2460
+ await new Promise(r => setTimeout(r, 200));
2461
+ done();
2462
+ });
2463
+
2464
+ // Handle login errors so they don't hang
2465
+ this.client.on('error', (err) => {
2466
+ if (!resolved) {
2467
+ clearTimeout(timeoutId);
2468
+ const msg = (err?.message || '').toLowerCase();
2469
+ if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401')) {
2470
+ this.log('error', `✗ TOKEN INVALID — this token is no longer valid`);
2471
+ this.paused = true;
2472
+ this._tokenInvalid = true;
2473
+ } else {
2474
+ this.log('error', `Login error: ${err?.message || err}`);
2381
2475
  }
2382
- // Load cached balance from Redis
2383
- try {
2384
- const balData = await redis.get(`dkg:bal:${this.account.id}`);
2385
- if (balData) {
2386
- const { wallet, bank } = JSON.parse(balData);
2387
- if (wallet > 0 || bank > 0) {
2388
- this.stats.balance = wallet;
2389
- this.stats.bankBalance = bank;
2390
- await fetch(`${API_URL}/api/grinder/status`, {
2391
- method: 'POST',
2392
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2393
- body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
2394
- }).catch(() => {});
2395
- }
2396
- }
2397
- } catch {}
2476
+ done();
2398
2477
  }
2399
-
2400
- // Let Discord gateway settle (reduced for faster startup)
2401
- await new Promise(r => setTimeout(r, 500));
2402
- resolve();
2403
2478
  });
2404
2479
 
2405
2480
  // Attach auto-recovery event listeners before login
2406
2481
  this._attachRecoveryListeners();
2407
- this.client.login(this.account.discord_token);
2482
+ this.client.login(this.account.discord_token).catch((err) => {
2483
+ clearTimeout(timeoutId);
2484
+ const msg = (err?.message || '').toLowerCase();
2485
+ if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401') || msg.includes('incorrect login')) {
2486
+ this.log('error', `✗ TOKEN INVALID — check this account's token`);
2487
+ this.paused = true;
2488
+ this._tokenInvalid = true;
2489
+ // Report invalid status to API
2490
+ fetch(`${API_URL}/api/grinder/status`, {
2491
+ method: 'POST',
2492
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2493
+ body: JSON.stringify({ account_id: this.account.id, active: false, status: 'token_invalid' }),
2494
+ }).catch(() => {});
2495
+ sendWebhook('Token Invalid', `**${this.account.label || this.account.id}** has an invalid Discord token.`, 0xef4444);
2496
+ } else {
2497
+ this.log('error', `Login failed: ${err?.message || 'unknown error'}`);
2498
+ }
2499
+ done();
2500
+ });
2408
2501
  });
2409
2502
  }
2410
2503
 
2504
+ /** Load Redis cached state in background (non-blocking) */
2505
+ async _loadRedisState() {
2506
+ if (!redis) return;
2507
+ for (const cmd of ['daily', 'weekly', 'monthly', 'drops']) {
2508
+ try {
2509
+ const val = await redis.get(`dkg:done:${this.account.id}:${cmd}`);
2510
+ if (val) {
2511
+ const ttlSec = await redis.ttl(`dkg:done:${this.account.id}:${cmd}`);
2512
+ if (ttlSec > 0) {
2513
+ this.doneToday.set(cmd, Date.now() + ttlSec * 1000);
2514
+ this.log('info', `${cmd} already claimed (${Math.round(ttlSec / 3600)}h remaining)`);
2515
+ }
2516
+ }
2517
+ } catch {}
2518
+ }
2519
+ // Load cached balance from Redis
2520
+ try {
2521
+ const balData = await redis.get(`dkg:bal:${this.account.id}`);
2522
+ if (balData) {
2523
+ const { wallet, bank } = JSON.parse(balData);
2524
+ if (wallet > 0 || bank > 0) {
2525
+ this.stats.balance = wallet;
2526
+ this.stats.bankBalance = bank;
2527
+ await fetch(`${API_URL}/api/grinder/status`, {
2528
+ method: 'POST',
2529
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2530
+ body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
2531
+ }).catch(() => {});
2532
+ }
2533
+ }
2534
+ } catch {}
2535
+ }
2536
+
2537
+ /** Retry login in background after a timeout */
2538
+ async _retryLoginBackground() {
2539
+ await new Promise(r => setTimeout(r, 5000));
2540
+ if (this.client && !this.running) {
2541
+ this.log('info', 'Retrying login in background...');
2542
+ try {
2543
+ this.client.destroy();
2544
+ this.client = createLeanClient();
2545
+ this._attachRecoveryListeners();
2546
+ await this.client.login(this.account.discord_token);
2547
+ this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2548
+ if (this.channel) {
2549
+ this.username = this.client.user?.tag || this.username;
2550
+ this.log('success', `Background login OK`);
2551
+ }
2552
+ } catch (err) {
2553
+ this.log('error', `Background login failed: ${err?.message || err}`);
2554
+ }
2555
+ }
2556
+ }
2557
+
2411
2558
  stop() {
2412
2559
  this.running = false;
2413
2560
  this.paused = false;
@@ -2537,13 +2684,19 @@ async function start(apiKey, apiUrl) {
2537
2684
  };
2538
2685
 
2539
2686
  // Parallel login in batches of 10 to avoid rate limits while being fast
2687
+ // Within each batch, stagger logins by 100-600ms to avoid gateway flood
2540
2688
  const BATCH_SIZE = 10;
2541
2689
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2542
2690
  if (shutdownCalled) break;
2543
2691
  const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
2544
2692
 
2545
- // Login batch in parallel
2693
+ // Staggered parallel login: fire each login with a small jitter delay
2546
2694
  await Promise.all(batch.map(async (acc, idx) => {
2695
+ // Stagger within batch: 0ms for first, 100-600ms for subsequent
2696
+ if (idx > 0) {
2697
+ const jitter = 100 + Math.floor(Math.random() * 500);
2698
+ await new Promise(r => setTimeout(r, jitter));
2699
+ }
2547
2700
  const worker = new AccountWorker(acc, i + idx);
2548
2701
  workers.push(worker);
2549
2702
  workerMap.set(acc.id, worker);
@@ -2560,21 +2713,33 @@ async function start(apiKey, apiUrl) {
2560
2713
  hintGC();
2561
2714
  }
2562
2715
 
2563
- // Phase 2: Run inventory on ALL accounts in parallel (must complete before grinding)
2564
- log('info', `${c.dim}Checking inventory for ${workers.length} accounts...${c.reset}`);
2716
+ // Login summary: show invalid tokens clearly
2717
+ const invalidWorkers = workers.filter(w => w._tokenInvalid);
2718
+ const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2719
+ if (invalidWorkers.length > 0) {
2720
+ console.log('');
2721
+ log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
2722
+ for (const w of invalidWorkers) {
2723
+ log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
2724
+ }
2725
+ console.log('');
2726
+ }
2727
+ if (timedOutWorkers.length > 0) {
2728
+ log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2729
+ }
2730
+
2731
+ // Filter out workers with invalid tokens from grinding
2732
+ const activeWorkers = workers.filter(w => !w._tokenInvalid);
2733
+
2734
+ // Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
2735
+ log('info', `${c.dim}Checking inventory for ${activeWorkers.length} accounts...${c.reset}`);
2565
2736
 
2566
2737
  // Parallel inventory checks with single-line progress
2567
2738
  let invDone = 0;
2568
2739
  let invFailed = 0;
2569
- const total = workers.length;
2570
-
2571
- await Promise.all(workers.map(async (w, i) => {
2572
- const label = w?.username || w?.account?.label || 'account';
2573
-
2574
- // Update progress on same line
2575
- const progress = `[inv] ${invDone + invFailed + 1}/${total}: ${label}`;
2576
- process.stdout.write(`\x1b[2K\r${c.dim}${progress}${c.reset}`);
2740
+ const total = activeWorkers.length;
2577
2741
 
2742
+ await Promise.all(activeWorkers.map(async (w, i) => {
2578
2743
  try {
2579
2744
  const invRes = await w.checkInventory({
2580
2745
  force: true,
@@ -2589,20 +2754,19 @@ async function start(apiKey, apiUrl) {
2589
2754
  }
2590
2755
  }));
2591
2756
 
2592
- // Final newline and summary
2593
- process.stdout.write('\n');
2757
+ // Final summary
2594
2758
  log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
2595
2759
 
2596
2760
  if (invFailed > 0) {
2597
- log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
2761
+ log('error', `${c.red}Inventory phase incomplete: ${invDone}/${activeWorkers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
2598
2762
  return;
2599
2763
  }
2600
2764
 
2601
2765
  const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
2602
- log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${workers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
2766
+ log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${activeWorkers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
2603
2767
 
2604
- // Phase 3: Start all grind loops
2605
- for (const w of workers) {
2768
+ // Phase 3: Start all grind loops (only for valid workers)
2769
+ for (const w of activeWorkers) {
2606
2770
  if (!shutdownCalled) w.grindLoop();
2607
2771
  }
2608
2772
 
@@ -2764,7 +2928,10 @@ function setupKeyboardShortcuts() {
2764
2928
  process.stdin.resume();
2765
2929
  process.stdin.setEncoding('utf8');
2766
2930
 
2767
- console.log(`\n ${c.dim}Keyboard shortcuts: ${c.reset}p=pause/resume all ${c.dim}·${c.reset} r=resume all ${c.dim}·${c.reset} s=status ${c.dim}·${c.reset} q=quit ${c.dim}·${c.reset} 1-9=toggle account`);
2931
+ // Modern styled keyboard shortcuts with box drawing
2932
+ console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2933
+ console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.bold}Shortcuts:${c.reset} ${c.dim}p${c.reset}=pause ${c.dim}r${c.reset}=resume ${c.dim}s${c.reset}=status ${c.dim}q${c.reset}=quit ${c.dim}?${c.reset}=help ${rgb(139, 92, 246)}│${c.reset}`);
2934
+ console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
2768
2935
 
2769
2936
  process.stdin.on('data', (key) => {
2770
2937
  const k = key.toString().toLowerCase();
@@ -2780,7 +2947,9 @@ function setupKeyboardShortcuts() {
2780
2947
  if (k === 'p') {
2781
2948
  let paused = 0;
2782
2949
  workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; paused++; } });
2783
- console.log(`\n ${c.yellow}Paused ${paused} accounts${c.reset}`);
2950
+ console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2951
+ console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.yellow}⏸ Paused ${paused} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
2952
+ console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
2784
2953
  return;
2785
2954
  }
2786
2955
 
@@ -2788,40 +2957,39 @@ function setupKeyboardShortcuts() {
2788
2957
  if (k === 'r') {
2789
2958
  let resumed = 0;
2790
2959
  workers.forEach(w => { if (w.paused) { w.paused = false; resumed++; } });
2791
- console.log(`\n ${c.green}Resumed ${resumed} accounts${c.reset}`);
2960
+ console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2961
+ console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.green}● Resumed ${resumed} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
2962
+ console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
2792
2963
  return;
2793
2964
  }
2794
2965
 
2795
2966
  // s = show status summary
2796
2967
  if (k === 's') {
2797
- console.log(`\n ${c.bold}Status Summary:${c.reset}`);
2798
2968
  const active = workers.filter(w => w.running && !w.paused).length;
2799
2969
  const paused = workers.filter(w => w.paused).length;
2800
2970
  const offline = workers.filter(w => !w.running).length;
2801
2971
  const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
2802
- console.log(` ${c.green}● ${active} active${c.reset} ${c.yellow}⏸ ${paused} paused${c.reset} ${c.red}○ ${offline} offline${c.reset} ${c.yellow}↻ ${recovering} recovering${c.reset}`);
2803
- console.log(` ${c.dim}Total earnings: ⏣ ${workers.reduce((s, w) => s + (w.stats.coins || 0), 0).toLocaleString()}${c.reset}`);
2804
- return;
2805
- }
2806
-
2807
- // 1-9 = toggle specific account (for small account counts)
2808
- const num = parseInt(k, 10);
2809
- if (num >= 1 && num <= 9 && workers[num - 1]) {
2810
- const w = workers[num - 1];
2811
- w.paused = !w.paused;
2812
- console.log(`\n ${w.color}${w.username}${c.reset} ${w.paused ? c.yellow + 'paused' : c.green + 'resumed'}${c.reset}`);
2972
+ const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
2973
+ console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
2974
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Status Summary${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
2975
+ console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
2976
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.green}● ${active} active${c.reset} ${c.yellow}⏸ ${paused} paused${c.reset} ${c.red}○ ${offline} offline${c.reset} ${c.yellow}↻ ${recovering} recovering${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
2977
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.dim}Total earnings:${c.reset} ${rgb(52, 211, 153)}⏣ ${totalEarn.toLocaleString()}${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
2978
+ console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
2813
2979
  return;
2814
2980
  }
2815
2981
 
2816
2982
  // ? = show help
2817
2983
  if (k === '?' || k === 'h') {
2818
- console.log(`\n ${c.bold}Keyboard Shortcuts:${c.reset}`);
2819
- console.log(` ${c.white}p${c.reset} Pause all accounts`);
2820
- console.log(` ${c.white}r${c.reset} Resume all accounts`);
2821
- console.log(` ${c.white}s${c.reset} Show status summary`);
2822
- console.log(` ${c.white}q${c.reset} Quit gracefully`);
2823
- console.log(` ${c.white}1-9${c.reset} Toggle account N`);
2824
- console.log(` ${c.white}?${c.reset} Show this help`);
2984
+ console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
2985
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Keyboard Shortcuts${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
2986
+ console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
2987
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}p${c.reset} Pause all accounts ${rgb(139, 92, 246)}║${c.reset}`);
2988
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}r${c.reset} Resume all accounts ${rgb(139, 92, 246)}║${c.reset}`);
2989
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}s${c.reset} Show status summary ${rgb(139, 92, 246)}║${c.reset}`);
2990
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}q${c.reset} Quit gracefully ${rgb(139, 92, 246)}║${c.reset}`);
2991
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}?${c.reset} Show this help ${rgb(139, 92, 246)}║${c.reset}`);
2992
+ console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
2825
2993
  return;
2826
2994
  }
2827
2995
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.0.0",
3
+ "version": "6.3.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"