dankgrinder 5.24.0 → 5.260.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/grinder.js CHANGED
@@ -316,37 +316,48 @@ function renderDashboard() {
316
316
  const tw = Math.min(process.stdout.columns || 80, 78);
317
317
  const thinBar = c.dim + '─'.repeat(tw) + c.reset;
318
318
  const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
319
+ const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
320
+ const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
319
321
 
320
322
  // Header with dynamic version, command count, and status
321
- lines.push(bar);
323
+ lines.push(doubleBar);
322
324
  const cmdCount = AccountWorker.COMMAND_MAP.length;
323
325
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
324
326
  const mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
327
+
328
+ // Animated spinner based on time
329
+ const spinners = ['◐', '◓', '◑', '◒'];
330
+ const spinner = spinners[Math.floor(Date.now() / 250) % 4];
331
+ const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
332
+
325
333
  lines.push(
326
334
  ` ${rgb(139, 92, 246)}${c.bold}DankGrinder${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}` +
327
335
  ` ${c.dim}·${c.reset} ${c.white}${cmdCount} Cmds${c.reset}` +
328
336
  ` ${c.dim}·${c.reset} ${mode}` +
329
- ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
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}`
330
338
  );
331
339
 
332
- // Stats row
333
- const liveIcon = rgb(52, 211, 153) + '' + c.reset;
340
+ // Stats row with enhanced visual indicators
341
+ const liveIcon = rgb(52, 211, 153) + '' + c.reset;
334
342
  const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
335
- const earnStr = `${rgb(52, 211, 153)} ${formatCoins(totalCoins)}${c.reset}`;
343
+ const earnStr = `${rgb(52, 211, 153)} ${formatCoins(totalCoins)}${c.reset}`;
336
344
  // Coins/hour rate
337
345
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
338
346
  const coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
339
347
  const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
340
348
  const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
341
349
  const rateStr = successRate >= 95
342
- ? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
343
- : successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
344
- : `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
345
- const upStr = `${rgb(251, 191, 36)} ${formatUptime()}${c.reset}`;
346
- // Memory usage (RSS in MB)
350
+ ? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
351
+ : successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
352
+ : `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
353
+ const upStr = `${rgb(251, 191, 36)} ${formatUptime()}${c.reset}`;
354
+ // Memory usage (RSS in MB) with bar indicator
347
355
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
356
+ const memPct = Math.min(100, (memMB / 1024) * 100);
357
+ const memBarWidth = Math.floor((memPct / 100) * 10);
358
+ const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
348
359
  const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
349
- const memStr = `${memColor}${memMB}MB${c.reset}`;
360
+ const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
350
361
  // Commands/minute from SlidingWindowCounter
351
362
  const cpmVal = globalCmdRate.getRate().toFixed(1);
352
363
  const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
@@ -394,9 +405,16 @@ function renderDashboard() {
394
405
  const bal = wk.stats.balance > 0
395
406
  ? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
396
407
  : `${c.dim}⏣ -${c.reset}`;
397
- const earned = wk.stats.coins > 0
398
- ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins).padStart(6)}${c.reset}`
399
- : `${c.dim} +0${c.reset}`;
408
+
409
+ // Mini progress bar for earned coins (visual indicator of activity)
410
+ const earnedNum = wk.stats.coins || 0;
411
+ const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
412
+ const earnedBar = earnedNum > 0
413
+ ? `${rgb(52, 211, 153)}${'▰'.repeat(earnedBarWidth)}${c.dim}${'▱'.repeat(5 - earnedBarWidth)}${c.reset}`
414
+ : `${c.dim}▱▱▱▱▱${c.reset}`;
415
+ const earned = earnedNum > 0
416
+ ? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
417
+ : `${c.dim}+0${c.reset} ${earnedBar}`;
400
418
 
401
419
  lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
402
420
  };
@@ -451,7 +469,7 @@ function renderDashboard() {
451
469
  }
452
470
  }
453
471
 
454
- lines.push(bar);
472
+ lines.push(doubleBarBot);
455
473
 
456
474
  // Absolute cursor home — always draw from row 1
457
475
  process.stdout.write('\x1b[H');
@@ -587,6 +605,51 @@ function safeParseJSON(str, fallback = []) {
587
605
  try { return JSON.parse(str || '[]'); } catch { return fallback; }
588
606
  }
589
607
 
608
+ // ── Command Result Formatter ──────────────────────────────────
609
+ // Clean command names and format results with color codes
610
+ const CMD_NAMES_CLEAN = {
611
+ bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
612
+ ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
613
+ hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
614
+ tidy: 'Tidy', farm: 'Farm', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly',
615
+ scratch: 'Scratch', adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
616
+ drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
617
+ work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
618
+ };
619
+
620
+ function formatCommandName(cmd) {
621
+ if (!cmd) return '?';
622
+ const clean = cmd.replace(/^pls\s+/, '').replace(/\s+\d+.*$/, '').trim().toLowerCase();
623
+ return CMD_NAMES_CLEAN[clean] || clean.charAt(0).toUpperCase() + clean.slice(1);
624
+ }
625
+
626
+ function formatCommandResult(cmdName, result, earned, spent) {
627
+ const name = formatCommandName(cmdName);
628
+ const net = (earned || 0) - (spent || 0);
629
+
630
+ // Extract clean result text
631
+ let cleanResult = stripAnsi(result || '').replace(/\n/g, ' ').substring(0, 40);
632
+
633
+ // Check for common failure/hold patterns
634
+ if (cleanResult.toLowerCase().includes('hold tight')) return `${name}: ${c.yellow}Hold Tight${c.reset}`;
635
+ if (cleanResult.toLowerCase().includes('cooldown')) return `${name}: ${c.dim}On Cooldown${c.reset}`;
636
+ if (cleanResult.toLowerCase().includes('no response')) return `${name}: ${c.red}No Response${c.reset}`;
637
+
638
+ // Format with earnings/losses
639
+ if (net > 0) {
640
+ return `${name}: ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`;
641
+ } else if (net < 0) {
642
+ return `${name}: ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`;
643
+ } else if (earned === 0 && spent === 0) {
644
+ // No coins changed - show result context
645
+ if (cleanResult.includes('completed') || cleanResult.includes('done')) {
646
+ return `${name}: ${c.dim}Done${c.reset}`;
647
+ }
648
+ return `${name}: ${c.dim}${cleanResult || 'Complete'}${c.reset}`;
649
+ }
650
+ return `${name}: ${c.dim}${cleanResult || 'Complete'}${c.reset}`;
651
+ }
652
+
590
653
  // ── Coin Parser — prefers Net:/Winnings: fields, falls back to max ⏣ ──
591
654
  function parseCoins(text) {
592
655
  if (!text) return 0;
@@ -764,6 +827,13 @@ class MinHeap {
764
827
  // ══════════════════════════════════════════════════════════════
765
828
 
766
829
  class AccountWorker {
830
+ static SMART_CD_FLOORS = Object.freeze({
831
+ 'farm': 30,
832
+ 'adventure': 300,
833
+ 'stream': 600,
834
+ 'work shift': 1800,
835
+ });
836
+
767
837
  constructor(account, idx) {
768
838
  this.account = account;
769
839
  this.idx = idx;
@@ -1114,7 +1184,10 @@ class AccountWorker {
1114
1184
  accountId: this.account.id,
1115
1185
  redis,
1116
1186
  onPageProgress: ({ page, total }) => {
1117
- this.log('info', `Inventory pages: ${page}/${total}`);
1187
+ // Update same line instead of spamming new lines - uses cursor control
1188
+ const progress = `[inv] ${this.username}: ${page}/${total} pages`;
1189
+ const erase = '\x1b[2K\r'; // Clear line + carriage return
1190
+ process.stdout.write(`${erase}${this.color}${progress}${c.reset}`);
1118
1191
  },
1119
1192
  });
1120
1193
 
@@ -1122,6 +1195,8 @@ class AccountWorker {
1122
1195
  throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
1123
1196
  }
1124
1197
 
1198
+ // Add newline after inventory progress
1199
+ process.stdout.write('\n');
1125
1200
  this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1126
1201
  try {
1127
1202
  await fetch(`${API_URL}/api/grinder/inventory`, {
@@ -1524,7 +1599,8 @@ class AccountWorker {
1524
1599
 
1525
1600
  const earned = Math.max(0, cmdResult.coins || 0);
1526
1601
  const spent = Math.max(0, cmdResult.lost || 0);
1527
- if (earned > 0) this.stats.coins += earned;
1602
+ // Track net earnings (add wins, subtract losses)
1603
+ this.stats.coins += (earned - spent);
1528
1604
  if (cmdResult.nextCooldownSec) {
1529
1605
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1530
1606
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
@@ -1571,8 +1647,9 @@ class AccountWorker {
1571
1647
  }
1572
1648
 
1573
1649
  this.stats.successes++;
1574
- const shortResult = result.substring(0, 30).replace(/\n/g, ' ');
1575
- this.setStatus(`${cmdName} ${shortResult}`);
1650
+ // Format result with clean command name and colored earnings
1651
+ const formattedResult = formatCommandResult(cmdName, result, earned, spent);
1652
+ this.setStatus(formattedResult);
1576
1653
  await sendLog(this.username, cmdName, result, 'success');
1577
1654
  reportEarnings(this.account.id, this.username, earned, spent, cmdName);
1578
1655
 
@@ -1667,7 +1744,7 @@ class AccountWorker {
1667
1744
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
1668
1745
  // Fast grinders — 10s CD
1669
1746
  { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
1670
- { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 6 },
1747
+ { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
1671
1748
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
1672
1749
  { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
1673
1750
  // Medium grinders — 20-25s CD
@@ -1681,10 +1758,10 @@ class AccountWorker {
1681
1758
  { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
1682
1759
  { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
1683
1760
  // Interactive — response-driven CD (handler sets nextCooldownSec)
1684
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 10, priority: 3 },
1685
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 10, priority: 3 },
1761
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
1762
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
1686
1763
  { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 10, priority: 3 },
1687
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 10, priority: 3 },
1764
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
1688
1765
  // Time-gated (run ASAP when available)
1689
1766
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
1690
1767
  { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
@@ -2029,8 +2106,6 @@ class AccountWorker {
2029
2106
  const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
2030
2107
  const totalWait = cd + jitterBase + microPause;
2031
2108
 
2032
- await this.setCooldown(item.cmd, totalWait);
2033
-
2034
2109
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
2035
2110
  const gapBase = cd <= 5 ? 1500 : cd <= 20 ? 2000 : 2500;
2036
2111
  const jitterGap = patternMod.minDelay + Math.random() * (patternMod.maxDelay - patternMod.minDelay);
@@ -2059,7 +2134,7 @@ class AccountWorker {
2059
2134
  // Grace period for interactive (button-click) commands — Dank Memer
2060
2135
  // needs time to process the interaction before accepting the next command.
2061
2136
  // Without this, the next command gets "Hold Tight" errors.
2062
- const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish']);
2137
+ const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
2063
2138
  if (INTERACTIVE_CMDS.has(item.cmd)) {
2064
2139
  await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
2065
2140
  }
@@ -2078,8 +2153,7 @@ class AccountWorker {
2078
2153
  this.failStreak = 0;
2079
2154
  }
2080
2155
 
2081
- this.lastCommandRun = Date.now();
2082
- await this.setCooldown(item.cmd, totalWait);
2156
+ this.lastCommandRun = Date.now();
2083
2157
 
2084
2158
  // Exponential backoff: if too many consecutive failures, slow down
2085
2159
  const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
@@ -2087,12 +2161,25 @@ class AccountWorker {
2087
2161
  const MIN_FAIL_COOLDOWN = 5;
2088
2162
 
2089
2163
  if (this.commandQueue && this.running && !shutdownCalled) {
2090
- let effectiveWait = this._lastCooldownOverride || totalWait;
2164
+ const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
2165
+ let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
2091
2166
  this._lastCooldownOverride = null;
2167
+
2168
+ // Smart fallback floors for long/interactive commands when parser misses exact cooldown.
2169
+ if (!hasOverride) {
2170
+ const floor = AccountWorker.SMART_CD_FLOORS[item.cmd];
2171
+ if (Number.isFinite(floor) && floor > 0) {
2172
+ effectiveWait = Math.max(effectiveWait, floor);
2173
+ }
2174
+ }
2175
+
2092
2176
  if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
2093
2177
  effectiveWait = MIN_FAIL_COOLDOWN;
2094
2178
  }
2095
- item.nextRunAt = Date.now() + effectiveWait * 1000 * backoffMultiplier;
2179
+
2180
+ const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
2181
+ await this.setCooldown(item.cmd, scheduledWaitSec);
2182
+ item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
2096
2183
  this.commandQueue.push(item);
2097
2184
  }
2098
2185
 
@@ -2311,8 +2398,8 @@ class AccountWorker {
2311
2398
  } catch {}
2312
2399
  }
2313
2400
 
2314
- // Let Discord gateway settle
2315
- await new Promise(r => setTimeout(r, 2500));
2401
+ // Let Discord gateway settle (reduced for faster startup)
2402
+ await new Promise(r => setTimeout(r, 500));
2316
2403
  resolve();
2317
2404
  });
2318
2405
 
@@ -2437,43 +2524,58 @@ async function start(apiKey, apiUrl) {
2437
2524
  console.log(` ${checks.join(' ')}`);
2438
2525
  console.log('');
2439
2526
 
2440
- // Phase 1: Login all accounts (staggered to avoid 429s)
2441
- const LOGIN_PROGRESS_EVERY = 5;
2442
- const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '100'), 10);
2443
- const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '300'), 10);
2444
- const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 100;
2445
- const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 300);
2527
+ // Phase 1: Login all accounts (optimized for speed)
2528
+ const LOGIN_PROGRESS_EVERY = 10;
2529
+ // Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
2530
+ const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2531
+ const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2532
+ const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
2533
+ const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 150);
2446
2534
 
2447
2535
  const randomLoginGap = () => {
2448
2536
  if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
2449
2537
  return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2450
2538
  };
2451
2539
 
2452
- for (let i = 0; i < accounts.length; i++) {
2540
+ // Parallel login in batches of 10 to avoid rate limits while being fast
2541
+ const BATCH_SIZE = 10;
2542
+ for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2453
2543
  if (shutdownCalled) break;
2454
- const worker = new AccountWorker(accounts[i], i);
2455
- workers.push(worker);
2456
- workerMap.set(accounts[i].id, worker);
2457
- await worker.start();
2458
- if (i + 1 < accounts.length) {
2544
+ const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
2545
+
2546
+ // Login batch in parallel
2547
+ await Promise.all(batch.map(async (acc, idx) => {
2548
+ const worker = new AccountWorker(acc, i + idx);
2549
+ workers.push(worker);
2550
+ workerMap.set(acc.id, worker);
2551
+ await worker.start();
2552
+ }));
2553
+
2554
+ // Small gap between batches
2555
+ if (i + BATCH_SIZE < accounts.length) {
2459
2556
  const gapMs = randomLoginGap();
2460
- if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2461
- log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next account in ${gapMs}ms...${c.reset}`);
2462
- }
2557
+ log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
2463
2558
  await new Promise(r => setTimeout(r, gapMs));
2464
2559
  }
2465
- if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2466
- hintGC();
2467
- }
2560
+
2561
+ hintGC();
2468
2562
  }
2469
2563
 
2470
2564
  // Phase 2: Run inventory on ALL accounts (must complete before any grinding)
2471
2565
  log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
2472
2566
  let invDone = 0;
2473
2567
  let invFailed = 0;
2474
- await Promise.all(workers.map(async (w, i) => {
2568
+
2569
+ // Use sequential inventory checks with single-line progress update
2570
+ const totalAccounts = workers.length;
2571
+ for (let i = 0; i < workers.length; i++) {
2572
+ const w = workers[i];
2475
2573
  const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2476
- log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label}${c.reset}`);
2574
+
2575
+ // Update single line instead of spamming new lines
2576
+ const progressLine = `\x1b[2K\r${c.dim}[inv] ${i + 1}/${totalAccounts}: ${label}${c.reset}`;
2577
+ process.stdout.write(progressLine);
2578
+
2477
2579
  try {
2478
2580
  const invRes = await w.checkInventory({
2479
2581
  force: true,
@@ -2486,9 +2588,11 @@ async function start(apiKey, apiUrl) {
2486
2588
  } catch {
2487
2589
  invFailed++;
2488
2590
  }
2489
- const invComplete = invDone + invFailed;
2490
- log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
2491
- }));
2591
+ }
2592
+
2593
+ // Newline after progress
2594
+ process.stdout.write('\n');
2595
+ log('success', `${c.dim}Inventory: ${invDone}/${workers.length} complete${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}${c.reset}`);
2492
2596
 
2493
2597
  if (invFailed > 0) {
2494
2598
  log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.24.0",
3
+ "version": "5.260.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"