dankgrinder 5.25.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.
Files changed (2) hide show
  1. package/lib/grinder.js +98 -29
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -605,6 +605,51 @@ function safeParseJSON(str, fallback = []) {
605
605
  try { return JSON.parse(str || '[]'); } catch { return fallback; }
606
606
  }
607
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
+
608
653
  // ── Coin Parser — prefers Net:/Winnings: fields, falls back to max ⏣ ──
609
654
  function parseCoins(text) {
610
655
  if (!text) return 0;
@@ -1139,7 +1184,10 @@ class AccountWorker {
1139
1184
  accountId: this.account.id,
1140
1185
  redis,
1141
1186
  onPageProgress: ({ page, total }) => {
1142
- 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}`);
1143
1191
  },
1144
1192
  });
1145
1193
 
@@ -1147,6 +1195,8 @@ class AccountWorker {
1147
1195
  throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
1148
1196
  }
1149
1197
 
1198
+ // Add newline after inventory progress
1199
+ process.stdout.write('\n');
1150
1200
  this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1151
1201
  try {
1152
1202
  await fetch(`${API_URL}/api/grinder/inventory`, {
@@ -1549,7 +1599,8 @@ class AccountWorker {
1549
1599
 
1550
1600
  const earned = Math.max(0, cmdResult.coins || 0);
1551
1601
  const spent = Math.max(0, cmdResult.lost || 0);
1552
- if (earned > 0) this.stats.coins += earned;
1602
+ // Track net earnings (add wins, subtract losses)
1603
+ this.stats.coins += (earned - spent);
1553
1604
  if (cmdResult.nextCooldownSec) {
1554
1605
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1555
1606
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
@@ -1596,8 +1647,9 @@ class AccountWorker {
1596
1647
  }
1597
1648
 
1598
1649
  this.stats.successes++;
1599
- const shortResult = result.substring(0, 30).replace(/\n/g, ' ');
1600
- 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);
1601
1653
  await sendLog(this.username, cmdName, result, 'success');
1602
1654
  reportEarnings(this.account.id, this.username, earned, spent, cmdName);
1603
1655
 
@@ -2346,8 +2398,8 @@ class AccountWorker {
2346
2398
  } catch {}
2347
2399
  }
2348
2400
 
2349
- // Let Discord gateway settle
2350
- await new Promise(r => setTimeout(r, 2500));
2401
+ // Let Discord gateway settle (reduced for faster startup)
2402
+ await new Promise(r => setTimeout(r, 500));
2351
2403
  resolve();
2352
2404
  });
2353
2405
 
@@ -2472,43 +2524,58 @@ async function start(apiKey, apiUrl) {
2472
2524
  console.log(` ${checks.join(' ')}`);
2473
2525
  console.log('');
2474
2526
 
2475
- // Phase 1: Login all accounts (staggered to avoid 429s)
2476
- const LOGIN_PROGRESS_EVERY = 5;
2477
- const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '100'), 10);
2478
- const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '300'), 10);
2479
- const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 100;
2480
- 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);
2481
2534
 
2482
2535
  const randomLoginGap = () => {
2483
2536
  if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
2484
2537
  return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2485
2538
  };
2486
2539
 
2487
- 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) {
2488
2543
  if (shutdownCalled) break;
2489
- const worker = new AccountWorker(accounts[i], i);
2490
- workers.push(worker);
2491
- workerMap.set(accounts[i].id, worker);
2492
- await worker.start();
2493
- 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) {
2494
2556
  const gapMs = randomLoginGap();
2495
- if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2496
- log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next account in ${gapMs}ms...${c.reset}`);
2497
- }
2557
+ log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
2498
2558
  await new Promise(r => setTimeout(r, gapMs));
2499
2559
  }
2500
- if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2501
- hintGC();
2502
- }
2560
+
2561
+ hintGC();
2503
2562
  }
2504
2563
 
2505
2564
  // Phase 2: Run inventory on ALL accounts (must complete before any grinding)
2506
2565
  log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
2507
2566
  let invDone = 0;
2508
2567
  let invFailed = 0;
2509
- 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];
2510
2573
  const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2511
- 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
+
2512
2579
  try {
2513
2580
  const invRes = await w.checkInventory({
2514
2581
  force: true,
@@ -2521,9 +2588,11 @@ async function start(apiKey, apiUrl) {
2521
2588
  } catch {
2522
2589
  invFailed++;
2523
2590
  }
2524
- const invComplete = invDone + invFailed;
2525
- log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
2526
- }));
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}`);
2527
2596
 
2528
2597
  if (invFailed > 0) {
2529
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.25.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"