dankgrinder 5.281.0 → 6.1.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 +156 -83
  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; });
@@ -2569,12 +2642,6 @@ async function start(apiKey, apiUrl) {
2569
2642
  const total = workers.length;
2570
2643
 
2571
2644
  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}`);
2577
-
2578
2645
  try {
2579
2646
  const invRes = await w.checkInventory({
2580
2647
  force: true,
@@ -2589,8 +2656,7 @@ async function start(apiKey, apiUrl) {
2589
2656
  }
2590
2657
  }));
2591
2658
 
2592
- // Final newline and summary
2593
- process.stdout.write('\n');
2659
+ // Final summary
2594
2660
  log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
2595
2661
 
2596
2662
  if (invFailed > 0) {
@@ -2764,7 +2830,10 @@ function setupKeyboardShortcuts() {
2764
2830
  process.stdin.resume();
2765
2831
  process.stdin.setEncoding('utf8');
2766
2832
 
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`);
2833
+ // Modern styled keyboard shortcuts with box drawing
2834
+ console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2835
+ 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}`);
2836
+ console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
2768
2837
 
2769
2838
  process.stdin.on('data', (key) => {
2770
2839
  const k = key.toString().toLowerCase();
@@ -2780,7 +2849,9 @@ function setupKeyboardShortcuts() {
2780
2849
  if (k === 'p') {
2781
2850
  let paused = 0;
2782
2851
  workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; paused++; } });
2783
- console.log(`\n ${c.yellow}Paused ${paused} accounts${c.reset}`);
2852
+ console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2853
+ console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.yellow}⏸ Paused ${paused} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
2854
+ console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
2784
2855
  return;
2785
2856
  }
2786
2857
 
@@ -2788,42 +2859,44 @@ function setupKeyboardShortcuts() {
2788
2859
  if (k === 'r') {
2789
2860
  let resumed = 0;
2790
2861
  workers.forEach(w => { if (w.paused) { w.paused = false; resumed++; } });
2791
- console.log(`\n ${c.green}Resumed ${resumed} accounts${c.reset}`);
2862
+ console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2863
+ console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.green}● Resumed ${resumed} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
2864
+ console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
2792
2865
  return;
2793
2866
  }
2794
2867
 
2795
2868
  // s = show status summary
2796
2869
  if (k === 's') {
2797
- console.log(`\n ${c.bold}Status Summary:${c.reset}`);
2798
2870
  const active = workers.filter(w => w.running && !w.paused).length;
2799
2871
  const paused = workers.filter(w => w.paused).length;
2800
2872
  const offline = workers.filter(w => !w.running).length;
2801
2873
  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}`);
2874
+ const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
2875
+ console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
2876
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Status Summary${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
2877
+ console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
2878
+ 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}`);
2879
+ 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}`);
2880
+ console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
2813
2881
  return;
2814
2882
  }
2815
2883
 
2816
2884
  // ? = show help
2817
2885
  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`);
2886
+ console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
2887
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Keyboard Shortcuts${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
2888
+ console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
2889
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}p${c.reset} Pause all accounts ${rgb(139, 92, 246)}║${c.reset}`);
2890
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}r${c.reset} Resume all accounts ${rgb(139, 92, 246)}║${c.reset}`);
2891
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}s${c.reset} Show status summary ${rgb(139, 92, 246)}║${c.reset}`);
2892
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}q${c.reset} Quit gracefully ${rgb(139, 92, 246)}║${c.reset}`);
2893
+ console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}?${c.reset} Show this help ${rgb(139, 92, 246)}║${c.reset}`);
2894
+ console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
2825
2895
  return;
2826
2896
  }
2827
2897
  });
2828
2898
  }
2829
2899
  }
2900
+
2901
+ // Export the start function for CLI
2902
+ module.exports = { start };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.281.0",
3
+ "version": "6.1.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"