dankgrinder 6.19.0 → 6.25.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.
@@ -16,11 +16,19 @@ const {
16
16
  const { Trie, VoseAlias, LRUCache } = require('../structures');
17
17
 
18
18
  const SAFE_CRIME_OPTIONS = Object.freeze([
19
- 'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
19
+ // 100% safe (from logs analysis)
20
+ 'identity theft', 'fraud', 'littering',
21
+ // 67% safe
22
+ 'dui',
23
+ // Other potentially safe options
24
+ 'tax evasion', 'cybercrime', 'hacking',
20
25
  'money laundering', 'tax fraud', 'insurance fraud', 'scam',
21
26
  ]);
22
27
 
23
28
  const RISKY_CRIME_OPTIONS = Object.freeze([
29
+ // 0% safe (from logs)
30
+ 'cyber bullying', 'trespassing', 'shoplifting',
31
+ // Known dangerous
24
32
  'murder', 'arson', 'assault', 'kidnap', 'terrorism',
25
33
  ]);
26
34
 
@@ -16,9 +16,12 @@ const {
16
16
  const { VoseAlias, Trie, EMA, LRUCache } = require('../structures');
17
17
 
18
18
  const SAFE_SEARCH_LOCATIONS = Object.freeze([
19
+ // 100% safe (from logs analysis)
20
+ 'shoe', 'washer', 'attic', 'pocket',
21
+ // Other safe locations
19
22
  'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
20
- 'couch', 'pantry', 'fridge', 'kitchen', 'bathroom', 'attic',
21
- 'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
23
+ 'couch', 'pantry', 'fridge', 'kitchen', 'bathroom',
24
+ 'closet', 'vacuum', 'toilet', 'sink', 'shower',
22
25
  'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
23
26
  ]);
24
27
 
package/lib/grinder.js CHANGED
@@ -458,7 +458,7 @@ function renderDashboard() {
458
458
  lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
459
459
  }
460
460
 
461
- lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
461
+ lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
462
462
 
463
463
  // Subtitle info
464
464
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
@@ -487,40 +487,62 @@ function renderDashboard() {
487
487
  lines.push(bEmpty);
488
488
 
489
489
  // ═══════════════════════════════════════════════════════════════
490
- // STATS PANEL
490
+ // STATS PANEL — left: metrics (fixed labels) | right: big trend
491
491
  // ═══════════════════════════════════════════════════════════════
492
492
  lines.push(bSep);
493
493
  lines.push(bEmpty);
494
494
 
495
- // Earnings sparkline data
496
495
  const now = Date.now();
497
496
  if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
498
- const sparkW = Math.min(30, Math.floor(iw * 0.3));
499
- const spark = drawSparkline(earningsHistory.toArray(), sparkW);
500
-
501
- // Row 1: Balance + Earned
502
497
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
503
498
  const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
504
- const peakFlag = isNewHigh ? ` ${R}${c.bold}* NEW HIGH *${c.reset}` : '';
505
-
506
- lines.push(bRow(` ${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣ ${formatCoins(totalBalance)}${c.reset} ${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}+⏣ ${formatCoins(totalCoins)}${c.reset} ${D}(${c.reset}${G}${formatCoins(perHr)}/h${c.reset}${D})${c.reset}${peakFlag}`));
507
-
508
- // Row 2: Peak + Trend sparkline
509
- lines.push(bRow(` ${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${A}~${c.reset} ${D}TREND${c.reset} ${spark}`));
510
499
 
511
- // Row 3: Commands + Success + Rate + Uptime
500
+ // ── Compute all metric values ──────────────────────────────────
512
501
  const cpmVal = globalCmdRate.getRate().toFixed(1);
513
502
  const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
514
- const srBarW = Math.min(15, Math.floor(iw * 0.12));
515
- const srBar = progressBar(successRate, 100, srBarW, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
516
- lines.push(bRow(` ${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
517
-
518
- // Row 4: Memory
503
+ const srBar = progressBar(successRate, 100, 10, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
519
504
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
520
505
  const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
521
- const memBarW = Math.min(20, Math.floor(iw * 0.15));
522
- const memBar = progressBar(memMB, 1024, memBarW, memCol, [40, 40, 55]);
523
- lines.push(bRow(` ${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`));
506
+ const memBar = progressBar(memMB, 1024, 10, memCol, [40, 40, 55]);
507
+ const perHrColor = perHr >= 0 ? G : R;
508
+ const perHrSign = perHr >= 0 ? '+' : '';
509
+ const newHighFlag = isNewHigh ? ` ${R}${c.bold}★ NEW HIGH ★${c.reset}` : '';
510
+
511
+ // ── Big trend sparkline (right column) ──────────────────────
512
+ const sparkW = Math.max(36, Math.floor(iw * 0.55));
513
+ const spark = drawSparkline(earningsHistory.toArray(), sparkW);
514
+
515
+ // ── Build each row: left metrics (fixed) | right trend ───────
516
+ const leftW = 36; // chars for left column content
517
+
518
+ // Helper: build a stat row with fixed-width label
519
+ const statRow = (icon, label, val) => {
520
+ const raw = `${icon} ${label} ${val}`;
521
+ const padded = raw + ' '.repeat(Math.max(0, leftW - raw.replace(RE, '').length));
522
+ return padded;
523
+ };
524
+
525
+ // Row 1: Balance
526
+ const balVal = `${Au}⏣${c.reset}${c.bold} ${formatCoins(totalBalance)}${c.reset}`;
527
+ const balRaw = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${balVal}`;
528
+ lines.push(bRow(` ${balRaw.padEnd(leftW + 4)}${A}~${c.reset} ${D}TREND${c.reset} ${spark}`));
529
+
530
+ // Row 2: Earned
531
+ const earnVal = `${perHrColor}${c.bold}${perHrSign}⏣ ${formatCoins(perHr)}/h${c.reset}`;
532
+ const earnRaw = `${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}${perHrSign}⏣ ${formatCoins(totalCoins)}${c.reset}${newHighFlag}`;
533
+ lines.push(bRow(` ${earnRaw.padEnd(leftW + 4)}${D}──┬──${c.reset} ${c.dim}earned over session${c.reset}`));
534
+
535
+ // Row 3: Peak
536
+ const peakRaw = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣ ${formatCoins(sessionPeakCoins)}${c.reset}`;
537
+ lines.push(bRow(` ${peakRaw.padEnd(leftW + 4)}${D} │${c.reset} ${c.dim}⏣ ${formatCoins(Math.abs(perHr))}/h${c.reset}`));
538
+
539
+ // Row 4: Commands
540
+ const cmdsRaw = `${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset}`;
541
+ lines.push(bRow(` ${cmdsRaw.padEnd(leftW + 4)}${D} │${c.reset}`));
542
+
543
+ // Row 5: Memory
544
+ const memRaw = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
545
+ lines.push(bRow(` ${memRaw.padEnd(leftW + 4)}${D} │${c.reset}`));
524
546
 
525
547
  lines.push(bEmpty);
526
548
 
@@ -630,9 +652,9 @@ function renderDashboard() {
630
652
  else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
631
653
  else lsStr = `${D}♥?${c.reset}`;
632
654
 
633
- // ── Level indicator ──
655
+ // ── Level indicator (fixed width so value changes don't jitter) ──
634
656
  const lvl = wk._level || 0;
635
- const lvlStr = lvl > 0 ? `${Cy}L${lvl}${c.reset}` : `${D}L?${c.reset}`;
657
+ const lvlStr = lvl > 0 ? `${Cy}L${String(lvl).padStart(3)}${c.reset}` : `${D}L???${c.reset}`;
636
658
 
637
659
  // ── Earned (fixed visible width) ──
638
660
  const earnNum = wk.stats.coins || 0;
@@ -1401,6 +1423,7 @@ class AccountWorker {
1401
1423
  startupProgress = null,
1402
1424
  requireComplete = false,
1403
1425
  maxAttempts = 1,
1426
+ silent = false,
1404
1427
  } = options;
1405
1428
  if (this._invRunning) return { ok: false, skipped: 'busy' };
1406
1429
  if (!force && this._lastInvCheck && Date.now() - this._lastInvCheck < 300_000) return { ok: false, skipped: 'recent' };
@@ -1414,7 +1437,7 @@ class AccountWorker {
1414
1437
  const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1415
1438
  const attemptLabel = tries > 1 ? ` [try ${attempt}/${tries}]` : '';
1416
1439
  const progressLine = `${baseLabel}${c.bold} ${this.username}${c.reset}${attemptLabel}`;
1417
- process.stdout.write(`\x1b[2K\r${progressLine}`);
1440
+ if (!silent) process.stdout.write(`\x1b[2K\r${progressLine}`);
1418
1441
 
1419
1442
  try {
1420
1443
  const result = await commands.runInventory({
@@ -1424,9 +1447,10 @@ class AccountWorker {
1424
1447
  accountId: this.account.id,
1425
1448
  redis,
1426
1449
  onPageProgress: ({ page, total }) => {
1427
- // Update progress on same line
1428
- const erase = '\x1b[2K\r';
1429
- process.stdout.write(`${erase}${baseLabel} ${c.bold}${this.username}${c.reset} · page ${page}/${total}${attemptLabel}`);
1450
+ if (!silent) {
1451
+ const erase = '\x1b[2K\r';
1452
+ process.stdout.write(`${erase}${baseLabel} ${c.bold}${this.username}${c.reset} · page ${page}/${total}${attemptLabel}`);
1453
+ }
1430
1454
  },
1431
1455
  });
1432
1456
 
@@ -1435,8 +1459,10 @@ class AccountWorker {
1435
1459
  }
1436
1460
 
1437
1461
  // Final result on same line
1438
- 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}`;
1439
- process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1462
+ if (!silent) {
1463
+ 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}`;
1464
+ process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1465
+ }
1440
1466
 
1441
1467
  // Extract lifesaver count from inventory and cache in Redis
1442
1468
  if (result.items && redis) {
@@ -1480,7 +1506,7 @@ class AccountWorker {
1480
1506
  if (attempt < tries) {
1481
1507
  const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1482
1508
  const retryLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.yellow}attempt ${attempt}/${tries} failed${c.reset} — retrying...`;
1483
- process.stdout.write(`\x1b[2K\r${retryLine}\n`);
1509
+ if (!silent) process.stdout.write(`\x1b[2K\r${retryLine}\n`);
1484
1510
  await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
1485
1511
  continue;
1486
1512
  }
@@ -1491,7 +1517,7 @@ class AccountWorker {
1491
1517
  } catch (e) {
1492
1518
  const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1493
1519
  const failLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.red}failed${c.reset} — ${e.message}`;
1494
- process.stdout.write(`\x1b[2K\r${failLine}\n`);
1520
+ if (!silent) process.stdout.write(`\x1b[2K\r${failLine}\n`);
1495
1521
  return { ok: false, error: e.message };
1496
1522
  } finally {
1497
1523
  this._invRunning = false;
@@ -1499,7 +1525,7 @@ class AccountWorker {
1499
1525
  }
1500
1526
  }
1501
1527
 
1502
- async checkBalance() {
1528
+ async checkBalance(silent = false) {
1503
1529
  const prefix = this.account.use_slash ? '/' : 'pls';
1504
1530
  const sentAt = Date.now();
1505
1531
 
@@ -1594,7 +1620,7 @@ class AccountWorker {
1594
1620
 
1595
1621
  this.stats.balance = wallet;
1596
1622
  this.stats.bankBalance = bank;
1597
- this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} ${c.dim}(${matched || 'none'})${c.reset}`);
1623
+ if (!silent) this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset}`);
1598
1624
 
1599
1625
  // Store in Redis for persistence
1600
1626
  if (redis) {
@@ -1619,13 +1645,17 @@ class AccountWorker {
1619
1645
  } catch { /* silent */ }
1620
1646
  }
1621
1647
 
1622
- // ── Check DM History for deaths/level-ups ──────────────────
1648
+ // ── Check DM History for deaths/level-ups (with retry) ─────
1623
1649
  async checkDmHistory() {
1624
- try {
1625
- const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
1626
- const dm = await dankUser.createDM();
1627
- this._dmChannelId = dm.id;
1628
- const recent = await dm.messages.fetch({ limit: 20 });
1650
+ const maxRetries = 3;
1651
+ const delays = [1000, 2000, 4000];
1652
+ let lastError;
1653
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1654
+ try {
1655
+ const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
1656
+ const dm = await dankUser.createDM();
1657
+ this._dmChannelId = dm.id;
1658
+ const recent = await dm.messages.fetch({ limit: 20 });
1629
1659
 
1630
1660
  let deaths = 0, levelUps = 0, currentLevel = 0, lastLifesaverCount = -1;
1631
1661
  for (const [, msg] of recent) {
@@ -1679,11 +1709,16 @@ class AccountWorker {
1679
1709
  }
1680
1710
  }
1681
1711
 
1682
- return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1683
- } catch (e) {
1684
- this.log('debug', `DM check failed: ${e.message}`);
1685
- return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1712
+ return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1713
+ } catch (e) {
1714
+ lastError = e;
1715
+ if (attempt < maxRetries - 1) {
1716
+ await new Promise(r => setTimeout(r, delays[attempt]));
1717
+ }
1718
+ }
1686
1719
  }
1720
+ this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1721
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1687
1722
  }
1688
1723
 
1689
1724
  // ── Run Single Command ──────────────────────────────────────
@@ -2993,25 +3028,82 @@ async function start(apiKey, apiUrl) {
2993
3028
  console.log(` ${checks.join(' ')}`);
2994
3029
  console.log('');
2995
3030
 
2996
- // ── Animated loading bar helper ──────────────────────────────
2997
- const barW = Math.min(40, (process.stdout.columns || 80) - 30);
2998
- let loginDone = 0;
2999
- const drawLoginProgress = () => {
3000
- const pct = accounts.length > 0 ? loginDone / accounts.length : 0;
3001
- const filled = Math.round(pct * barW);
3002
- const empty = barW - filled;
3003
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3004
- const bar = rgb(139, 92, 246) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
3005
- const pctStr = `${Math.round(pct * 100)}%`;
3006
- process.stdout.write(`\r ${rgb(139, 92, 246)}${spin}${c.reset} ${c.dim}Logging in...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}${pctStr}${c.reset} `);
3031
+ // ── Per-account inline login UI ──────────────────────────────
3032
+ // Track login state per account for inline rendering
3033
+ const loginStates = accounts.map((acc, i) => ({
3034
+ name: acc.label || acc.id || '?',
3035
+ done: false,
3036
+ failed: false,
3037
+ worker: null,
3038
+ workerIdx: i,
3039
+ }));
3040
+
3041
+ // Column widths (visible chars)
3042
+ const terminalW = process.stdout.columns || 90;
3043
+ const colNum = 4; // " # "
3044
+ const colSts = 3; // "ST "
3045
+ const colName = Math.min(24, Math.max(12, Math.floor(terminalW * 0.25)));
3046
+ const colGuild = Math.min(20, Math.max(8, Math.floor(terminalW * 0.2)));
3047
+ const colCmds = 10; // " 20 cmds"
3048
+ const totalVis = colNum + colSts + colName + colGuild + colCmds + 8;
3049
+
3050
+ // Print header + all account lines (initial pending state)
3051
+ console.log(` ${'─'.repeat(totalVis)}`);
3052
+ for (let i = 0; i < loginStates.length; i++) {
3053
+ const s = loginStates[i];
3054
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3055
+ const name = s.name.substring(0, colName).padEnd(colName);
3056
+ console.log(`${num}${c.dim}PN${c.reset} ${name} ${c.dim}${'···'.padEnd(colGuild)}${c.reset} ${c.dim}···${c.reset}`);
3057
+ }
3058
+ console.log(` ${'─'.repeat(totalVis)}`);
3059
+
3060
+ // Draw pending spinners
3061
+ const drawLoginSpinners = () => {
3062
+ for (let i = 0; i < loginStates.length; i++) {
3063
+ const s = loginStates[i];
3064
+ if (s.done || s.failed) continue;
3065
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3066
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3067
+ const name = s.name.substring(0, colName).padEnd(colName);
3068
+ const guild = 'logging in...'.substring(0, colGuild).padEnd(colGuild);
3069
+ process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${c.dim}${guild}${c.reset} ${c.dim}···${c.reset}`);
3070
+ }
3071
+ process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3007
3072
  };
3073
+ const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3074
+
3075
+ // Update a line when an account finishes
3076
+ const finalizeLoginLine = (idx, worker) => {
3077
+ const s = loginStates[idx];
3078
+ if (s.done || s.failed) return;
3079
+ s.done = true;
3080
+ s.worker = worker;
3081
+
3082
+ const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset} `;
3083
+ const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3084
+
3085
+ let sts, guild, cmds;
3086
+ if (worker._tokenInvalid) {
3087
+ sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3088
+ guild = 'INVALID'.padEnd(colGuild);
3089
+ cmds = '···';
3090
+ s.failed = true;
3091
+ } else if (worker.channel) {
3092
+ sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3093
+ const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
3094
+ guild = gn.padEnd(colGuild);
3095
+ cmds = `${worker.stats?.commands || 0}cmds`;
3096
+ } else {
3097
+ sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
3098
+ guild = 'timeout'.padEnd(colGuild);
3099
+ cmds = '···';
3100
+ }
3008
3101
 
3009
- // Progress animation timer
3010
- const progressInterval = setInterval(drawLoginProgress, 80);
3102
+ const line = ` ${num}${sts} ${name} ${guild} ${cmds}`;
3103
+ process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3104
+ };
3011
3105
 
3012
- // Phase 1: Login all accounts (optimized for speed)
3013
- const LOGIN_PROGRESS_EVERY = 10;
3014
- // Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
3106
+ // Phase 1: Login in batches of 10
3015
3107
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
3016
3108
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
3017
3109
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
@@ -3022,16 +3114,12 @@ async function start(apiKey, apiUrl) {
3022
3114
  return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
3023
3115
  };
3024
3116
 
3025
- // Parallel login in batches of 10 to avoid rate limits while being fast
3026
- // Within each batch, stagger logins by 100-600ms to avoid gateway flood
3027
3117
  const BATCH_SIZE = 10;
3028
3118
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
3029
3119
  if (shutdownCalled) break;
3030
3120
  const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
3031
3121
 
3032
- // Staggered parallel login: fire each login with a small jitter delay
3033
3122
  await Promise.all(batch.map(async (acc, idx) => {
3034
- // Stagger within batch: 0ms for first, 100-600ms for subsequent
3035
3123
  if (idx > 0) {
3036
3124
  const jitter = 100 + Math.floor(Math.random() * 500);
3037
3125
  await new Promise(r => setTimeout(r, jitter));
@@ -3039,30 +3127,29 @@ async function start(apiKey, apiUrl) {
3039
3127
  const worker = new AccountWorker(acc, i + idx);
3040
3128
  workers.push(worker);
3041
3129
  workerMap.set(acc.id, worker);
3130
+ loginStates[i + idx].worker = worker;
3042
3131
  await worker.start();
3043
- loginDone++;
3132
+ finalizeLoginLine(i + idx, worker);
3044
3133
  }));
3045
3134
 
3046
- // Small gap between batches
3047
3135
  if (i + BATCH_SIZE < accounts.length) {
3048
- const gapMs = randomLoginGap();
3049
- await new Promise(r => setTimeout(r, gapMs));
3136
+ await new Promise(r => setTimeout(r, randomLoginGap()));
3050
3137
  }
3051
3138
 
3052
3139
  hintGC();
3053
3140
  }
3054
3141
 
3055
- clearInterval(progressInterval);
3056
- // Clear the progress line and show done
3057
- process.stdout.write(`\r${c.clearLine}`);
3058
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}/${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3059
- console.log('');
3142
+ clearInterval(loginSpinnerInterval);
3143
+ process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3060
3144
 
3061
- // Login summary: show invalid tokens clearly
3145
+ // Final summary
3146
+ const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3062
3147
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3063
3148
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3149
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3150
+ console.log('');
3151
+
3064
3152
  if (invalidWorkers.length > 0) {
3065
- console.log('');
3066
3153
  log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3067
3154
  for (const w of invalidWorkers) {
3068
3155
  log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
@@ -3076,85 +3163,176 @@ async function start(apiKey, apiUrl) {
3076
3163
  // Filter out workers with invalid tokens from grinding
3077
3164
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3078
3165
 
3079
- // Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
3080
- console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking inventory for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${c.dim} accounts...${c.reset}`);
3081
-
3082
- // Animated inventory progress
3083
- let invDone = 0;
3084
- let invFailed = 0;
3085
- const total = activeWorkers.length;
3086
- const invBarW = Math.min(40, (process.stdout.columns || 80) - 30);
3087
-
3088
- const drawInvProgress = () => {
3089
- const pct = total > 0 ? invDone / total : 0;
3090
- const filled = Math.round(pct * invBarW);
3091
- const empty = invBarW - filled;
3092
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3093
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
3094
- process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${total}${c.reset} ${c.dim}${Math.round(pct * 100)}%${c.reset} `);
3166
+ // ── Phase 2: Inventory check with per-account inline rendering ─────────────────
3167
+ const invStates = activeWorkers.map((w, i) => ({
3168
+ name: w.username || w.account.label || '?',
3169
+ idx: i,
3170
+ done: false,
3171
+ failed: false,
3172
+ items: 0,
3173
+ value: 0,
3174
+ attempt: 0,
3175
+ worker: w,
3176
+ }));
3177
+
3178
+ const tw2 = process.stdout.columns || 90;
3179
+ const iColNum = 4;
3180
+ const iColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3181
+ const iColItems = 8;
3182
+ const iColVal = 14;
3183
+ const iColTries = 10;
3184
+ const iTotalVis = iColNum + iColName + iColItems + iColVal + iColTries + 10;
3185
+
3186
+ console.log(` ${'─'.repeat(iTotalVis)}`);
3187
+ for (let i = 0; i < invStates.length; i++) {
3188
+ const s = invStates[i];
3189
+ const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3190
+ const name = s.name.substring(0, iColName).padEnd(iColName);
3191
+ console.log(`${num}${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'··'.padEnd(iColVal)}${c.reset} ${c.dim}${''.padEnd(iColTries)}${c.reset}`);
3192
+ }
3193
+ console.log(` ${'─'.repeat(iTotalVis)}`);
3194
+
3195
+ let invDone = 0, invFailed = 0;
3196
+
3197
+ const drawInvSpinners = () => {
3198
+ for (let i = 0; i < invStates.length; i++) {
3199
+ const s = invStates[i];
3200
+ if (s.done || s.failed) continue;
3201
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3202
+ const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3203
+ const name = s.name.substring(0, iColName).padEnd(iColName);
3204
+ const items = 'checking'.substring(0, iColItems).padEnd(iColItems);
3205
+ const val = ''.padEnd(iColVal);
3206
+ const tries = s.attempt > 0 ? `try ${s.attempt}/3` : '';
3207
+ process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(34, 211, 238)}${spin}${c.reset} ${name} ${c.dim}${items}${c.reset} ${c.dim}${val}${c.reset} ${c.dim}${tries}${c.reset}`);
3208
+ }
3209
+ process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3095
3210
  };
3211
+ const invSpinnerInterval = setInterval(drawInvSpinners, 80);
3212
+
3213
+ const finalizeInvLine = (idx, invRes) => {
3214
+ const s = invStates[idx];
3215
+ if (s.done || s.failed) return;
3216
+ if (invRes?.ok) {
3217
+ s.done = true;
3218
+ invDone++;
3219
+ } else {
3220
+ s.failed = true;
3221
+ invFailed++;
3222
+ }
3096
3223
 
3097
- const invProgressInterval = setInterval(drawInvProgress, 80);
3224
+ const num = `${c.dim}${(idx + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3225
+ const name = s.name.substring(0, iColName).padEnd(iColName);
3226
+ const items = `${invRes?.result?.items?.length || 0}`.padEnd(iColItems);
3227
+ const val = invRes?.ok
3228
+ ? `${c.green}⏣${(invRes.result?.totalValue || 0).toLocaleString()}${c.reset}`.padEnd(iColVal + 3)
3229
+ : `${c.dim}··${c.reset}`.padEnd(iColVal);
3230
+ const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3231
+
3232
+ const line = ` ${num}${sts} ${name} ${items} ${val}`;
3233
+ process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3234
+ };
3098
3235
 
3099
3236
  await Promise.all(activeWorkers.map(async (w, i) => {
3100
3237
  try {
3101
- const invRes = await w.checkInventory({
3102
- force: true,
3103
- startupProgress: { current: i + 1, total },
3104
- requireComplete: true,
3105
- maxAttempts: 3,
3106
- });
3107
- if (invRes?.ok) invDone++;
3108
- else invFailed++;
3238
+ const invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true });
3239
+ finalizeInvLine(i, invRes);
3109
3240
  } catch {
3110
- invFailed++;
3241
+ finalizeInvLine(i, { ok: false });
3111
3242
  }
3112
3243
  }));
3113
3244
 
3114
- clearInterval(invProgressInterval);
3115
- process.stdout.write(`\r${c.clearLine}`);
3245
+ clearInterval(invSpinnerInterval);
3246
+ process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3116
3247
 
3117
- // Final summary
3118
3248
  if (invFailed > 0) {
3119
- console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}Inventory incomplete${c.reset} ${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${total}${c.reset} done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
3249
+ console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}Inventory incomplete${c.reset} ${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
3120
3250
  log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
3121
3251
  return;
3122
3252
  }
3123
3253
 
3124
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${total}${c.reset} ${c.dim}all clear${c.reset}`);
3254
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${activeWorkers.length}${c.reset} ${c.dim}all clear${c.reset}`);
3125
3255
  console.log('');
3126
3256
 
3127
- // Phase 2.5: Check balance for ALL accounts sequentially (CV2 needs raw logger timing)
3128
- console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking balance for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${c.dim} accounts...${c.reset}`);
3257
+ // ── Phase 2.5: Balance check with inline per-account rendering ─────────────────
3258
+ const balStates = activeWorkers.map((w, i) => ({
3259
+ name: w.username || w.account.label || '?',
3260
+ idx: i,
3261
+ done: false,
3262
+ wallet: 0,
3263
+ bank: 0,
3264
+ worker: w,
3265
+ }));
3266
+
3267
+ const bColNum = 4;
3268
+ const bColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3269
+ const bColWallet = 14;
3270
+ const bColBank = 14;
3271
+ const bColTotal = 14;
3272
+ const bColLs = 4;
3273
+ const bTotalVis = bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 12;
3274
+
3275
+ console.log(` ${'─'.repeat(bTotalVis)}`);
3276
+ for (let i = 0; i < balStates.length; i++) {
3277
+ const s = balStates[i];
3278
+ const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3279
+ const name = s.name.substring(0, bColName).padEnd(bColName);
3280
+ console.log(`${num}${c.dim}··${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'··'.padEnd(bColBank)}${c.reset} ${c.dim}${'··'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3281
+ }
3282
+ console.log(` ${'─'.repeat(bTotalVis)}`);
3129
3283
 
3130
3284
  let balDone = 0;
3131
- const balProgressInterval = setInterval(() => {
3132
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3133
- process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${c.bold}${rgb(52, 211, 153)}${balDone}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} `);
3134
- }, 80);
3135
3285
 
3136
- // Run in parallel
3286
+ const drawBalSpinners = () => {
3287
+ for (let i = 0; i < balStates.length; i++) {
3288
+ const s = balStates[i];
3289
+ if (s.done) continue;
3290
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3291
+ const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3292
+ const name = s.name.substring(0, bColName).padEnd(bColName);
3293
+ process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(251, 191, 36)}${spin}${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'··'.padEnd(bColBank)}${c.reset} ${c.dim}${'··'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3294
+ }
3295
+ process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3296
+ };
3297
+ const balSpinnerInterval = setInterval(drawBalSpinners, 80);
3298
+
3299
+ const finalizeBalLine = (idx, w) => {
3300
+ const s = balStates[idx];
3301
+ if (s.done) return;
3302
+ s.done = true;
3303
+ s.wallet = w.stats?.balance || 0;
3304
+ s.bank = w.stats?.bankBalance || 0;
3305
+ balDone++;
3306
+
3307
+ const num = `${c.dim}${(idx + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3308
+ const name = (w.username || s.name || '?').substring(0, bColName).padEnd(bColName);
3309
+ const ls = w._lifesavers ?? '?';
3310
+ const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3311
+ const wallet = `${c.green}⏣${(s.wallet).toLocaleString()}${c.reset}`.padEnd(bColWallet + 3);
3312
+ const bank = `${c.cyan}⏣${(s.bank).toLocaleString()}${c.reset}`.padEnd(bColBank + 3);
3313
+ const total = `${c.bold}⏣${(s.wallet + s.bank).toLocaleString()}${c.reset}`.padEnd(bColTotal + 3);
3314
+
3315
+ const line = ` ${num}${rgb(52, 211, 153)}✓${c.reset} ${name} ${wallet} ${bank} ${total} ${lsColor}♥${ls}${c.reset}`;
3316
+ process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3317
+ };
3318
+
3137
3319
  await Promise.all(activeWorkers.map(async w => {
3138
3320
  try {
3139
- await w.checkBalance();
3140
- balDone++;
3321
+ await w.checkBalance(true); // silent: don't spam console during inline rendering
3141
3322
  } catch {}
3323
+ const idx = balStates.findIndex(s => s.worker === w);
3324
+ if (idx >= 0) finalizeBalLine(idx, w);
3142
3325
  }));
3143
3326
 
3144
- clearInterval(balProgressInterval);
3145
- process.stdout.write(`\r${c.clearLine}`);
3327
+ clearInterval(balSpinnerInterval);
3328
+ process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3146
3329
 
3147
- // Show balance + lifesaver summary for each account
3330
+ // Balance summary
3148
3331
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3149
- for (const w of activeWorkers) {
3150
- const wallet = w.stats?.balance || 0;
3151
- const bank = w.stats?.bankBalance || 0;
3152
- const ls = w._lifesavers ?? '?';
3153
- totalWallet += wallet;
3154
- totalBank += bank;
3155
- const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3156
- console.log(` ${c.dim}├${c.reset} ${c.bold}${w.username}${c.reset} Wallet: ${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} LS: ${lsColor}${ls}${c.reset}`);
3157
- if (ls === 0) noLifesaverAccounts.push(w.username);
3332
+ for (const s of balStates) {
3333
+ totalWallet += s.wallet;
3334
+ totalBank += s.bank;
3335
+ if (s.worker._lifesavers === 0) noLifesaverAccounts.push(s.name);
3158
3336
  }
3159
3337
  console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Balance${c.reset} Total: ${c.bold}${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
3160
3338
 
package/lib/rawLogger.js CHANGED
@@ -152,18 +152,30 @@ function detectCommand(d) {
152
152
  if (cv2Text.includes('fishing') || cv2Text.includes('fisherfolk')) return 'fish';
153
153
  if (cv2Text.includes('deposit') || cv2Text.includes('bank account')) return 'deposit';
154
154
  if (cv2Text.includes('begging') || cv2Text.includes('imagine begging')) return 'beg';
155
- if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle')) return 'hunt';
156
- if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig')) return 'dig';
155
+ if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle') || cv2Text.includes('your aim was so bad') || cv2Text.includes('animals laughed') || cv2Text.includes('animals attacked') || cv2Text.includes('barely escaped') || cv2Text.includes('fell asleep in a tree') || cv2Text.includes('caught nothing') || cv2Text.includes('brought back literally nothing') || cv2Text.includes('rifle broke')) return 'hunt';
156
+ if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig') || cv2Text.includes('dug in the dirt') || cv2Text.includes('brought back') && (cv2Text.includes('ant') || cv2Text.includes('worm') || cv2Text.includes('stickbug') || cv2Text.includes('ladybug'))) return 'dig';
157
157
  if (cv2Text.includes('great work') || cv2Text.includes('for your shift') || cv2Text.includes('working as') || cv2Text.includes('work shift') || cv2Text.includes('what color was') || cv2Text.includes('remember words order') || cv2Text.includes('remember the colors') || cv2Text.includes('remember the emojis') || cv2Text.includes('what word was repeated') || cv2Text.includes('unscramble') || cv2Text.includes('remember the number') || cv2Text.includes('click the buttons in correct order') || cv2Text.includes('babysitter') || cv2Text.includes('click the matching')) return 'work';
158
158
  if (cv2Text.includes('weekly')) return 'weekly';
159
159
  if (cv2Text.includes('daily')) return 'daily';
160
160
  if (cv2Text.includes('inventory')) return 'inventory';
161
161
  if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
162
+ if (cv2Text.includes('balances') && cv2Text.includes('global rank')) return 'balance';
163
+
164
+ // Check content text (plain message content)
165
+ const contentText = (d.content || '').toLowerCase();
166
+ if (contentText.includes('balances') && contentText.includes('global rank')) return 'balance';
167
+ if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed')) return 'hunt';
168
+ if (contentText.includes('imagine going into the woods')) return 'hunt';
169
+ if (contentText.includes('you ran an ad for') && contentText.includes('received')) return 'stream';
170
+ if (contentText.includes('you can\'t interact with your stream')) return 'stream';
171
+ if (contentText.includes('you dug in the dirt') || contentText.includes('found nothing while digging')) return 'dig';
162
172
 
163
173
  // Check embed text
164
174
  const embedText = extractEmbedText(d.embeds).toLowerCase();
165
175
  // Gambling
166
176
  if (embedText.includes('high') && embedText.includes('low') && embedText.includes('secret number')) return 'highlow';
177
+ if (embedText.includes('you won') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
178
+ if (embedText.includes('you lost') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
167
179
  if (embedText.includes('blackjack') || embedText.includes('dealer')) return 'blackjack';
168
180
  if (embedText.includes('roulette')) return 'roulette';
169
181
  if (embedText.includes('spinning') && embedText.includes('slots')) return 'slots';
@@ -175,10 +187,18 @@ function detectCommand(d) {
175
187
  if (embedText.includes('what crime do you want')) return 'crime';
176
188
  if (embedText.includes('where do you want to search')) return 'search';
177
189
  if (embedText.includes('you searched') || embedText.includes('searched the')) return 'search';
190
+ if (embedText.includes('committed') && (embedText.includes('trespassing') || embedText.includes('identity theft') || embedText.includes('fraud') || embedText.includes('shoplifting') || embedText.includes('dui') || embedText.includes('tax evasion') || embedText.includes('littering') || embedText.includes('cyber bullying') || embedText.includes('grand theft auto') || embedText.includes('drug distribution') || embedText.includes('bank robbing') || embedText.includes('arson') || embedText.includes('murder') || embedText.includes('vandalism') || embedText.includes('jaywalking') || embedText.includes('piracy') || embedText.includes('breaking and entering'))) return 'crime';
178
191
  if (embedText.includes('you committed') || embedText.includes('went outside')) return 'crime';
192
+ if (embedText.includes('stole a developer') || embedText.includes('got confused about what trespassing')) return 'crime';
193
+ // Search results (person names) - also check for beg results
194
+ if ((embedText.includes('oh you poor soul') || embedText.includes('take this') || embedText.includes('sure take') || embedText.includes('here\'s a thought') || embedText.includes('nope, nothing') || embedText.includes('no u') || embedText.includes('coins? in this economy')) && (embedText.includes('###') || embedText.includes('charlie chaplin') || embedText.includes('shrek') || embedText.includes('elton john') || embedText.includes('alexa') || embedText.includes('confucius') || embedText.includes('doctor strange') || embedText.includes('rick astley') || embedText.includes('toby turner') || embedText.includes('oprah') || embedText.includes('bruce lee') || embedText.includes('david attenborough') || embedText.includes('honey badger'))) {
195
+ // Check if it's a beg result (has life saver or specific beg text)
196
+ if (embedText.includes('life saver') || embedText.includes('lifesaver')) return 'beg';
197
+ return 'search';
198
+ }
179
199
  // Hunt / dig
180
- if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('hunt') && (embedText.includes('caught') || embedText.includes('brought back') || embedText.includes('attacked') || embedText.includes('nothing') || embedText.includes('laughed') || embedText.includes('escaped') || embedText.includes('fell asleep'))) return 'hunt';
181
- if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('found a') && embedText.includes('dig') || embedText.includes('shovel') || embedText.includes('found nothing while') || embedText.includes('you found') && !embedText.includes('search')) return 'dig';
200
+ if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('your aim was so bad') || embedText.includes('animals laughed') || embedText.includes('animals attacked') || embedText.includes('barely escaped') || embedText.includes('fell asleep in a tree') || embedText.includes('caught nothing') || embedText.includes('brought back literally nothing') || embedText.includes('rifle broke') || embedText.includes('imagine going into the woods')) return 'hunt';
201
+ if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('dug in the dirt') || embedText.includes('found nothing while') || embedText.includes('what are the odds lol') || embedText.includes('brought back') && (embedText.includes('ant') || embedText.includes('worm') || embedText.includes('stickbug') || embedText.includes('ladybug'))) return 'dig';
182
202
  // Work — match both minigame prompt AND completion
183
203
  if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color') || embedText.includes('babysitter') || embedText.includes('great work') || embedText.includes('for your shift'))) return 'work';
184
204
  if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
@@ -187,11 +207,20 @@ function detectCommand(d) {
187
207
  // Postmemes
188
208
  if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
189
209
  // Stream
190
- if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream')) return 'stream';
210
+ if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream') || embedText.includes('you ran an ad for') || embedText.includes('you received') && embedText.includes('from your sponsors') || embedText.includes('### chat') && embedText.includes('hasanbabi')) return 'stream';
211
+ if (embedText.includes('you can\'t interact with your stream') || embedText.includes('stream can last')) return 'stream';
191
212
  // Deposit
192
213
  if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
214
+ // Balance
215
+ if (embedText.includes('balances') && embedText.includes('global rank') && embedText.includes('net worth')) return 'balance';
193
216
  // Trivia
194
- if (embedText.includes('you have 10 seconds to answer') || embedText.includes('trivia')) return 'trivia';
217
+ if (embedText.includes('you have 10 seconds to answer') || embedText.includes('you have 12 seconds to answer') || embedText.includes('you have 15 seconds to answer') || embedText.includes('trivia') || embedText.includes('difficulty') && embedText.includes('category') && (embedText.includes('correct answer was') || embedText.includes('you got that answer correct'))) return 'trivia';
218
+ if (embedText.includes('who in pulp fiction') || embedText.includes('what was') || embedText.includes('which of')) return 'trivia';
219
+ // Cooldown messages
220
+ if (embedText.includes('you can work again at') || embedText.includes('you can use this command again')) return 'cooldown';
221
+ if (embedText.includes('amount needs to be greater than 0')) return 'cooldown';
222
+ // Premium/upgrade messages
223
+ if (embedText.includes('you can buy the ability to use this command')) return 'premium';
195
224
  // Profile / level
196
225
  if (embedText.includes('level:') && embedText.includes('experience:')) return 'profile';
197
226
  // Shop
@@ -218,7 +247,15 @@ function parseRawPacket(d, event) {
218
247
  const embedText = extractEmbedText(d.embeds);
219
248
  const isCV2 = !!(d.flags & 32768);
220
249
  const isEphemeral = !!(d.flags & 64);
221
- const command = detectCommand(d);
250
+
251
+ // For UPDATE events, try to preserve the original command classification
252
+ let command = detectCommand(d);
253
+ if (event === 'UPDATE' && command === 'unknown') {
254
+ const existing = memStore.get(d.id);
255
+ if (existing && existing.command && existing.command !== 'unknown') {
256
+ command = existing.command;
257
+ }
258
+ }
222
259
 
223
260
  return {
224
261
  id: d.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.19.0",
3
+ "version": "6.25.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"