dankgrinder 6.8.2 → 6.16.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
@@ -2,6 +2,7 @@ const { Client, Options } = require('discord.js-selfbot-v13');
2
2
  const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
4
  const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
5
+ const rawLogger = require('./rawLogger');
5
6
  const {
6
7
  BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
7
8
  AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
@@ -430,10 +431,34 @@ function renderDashboard() {
430
431
  lines.push(bTop);
431
432
  lines.push(bEmpty);
432
433
 
433
- // Title with animated spinner
434
434
  const spin = getSpinner('braille');
435
- const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
436
- lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
435
+
436
+ // Title — big gradient banner
437
+ const titleLines = [
438
+ '██████╗ █████╗ ███╗ ██╗██╗ ██╗ ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗',
439
+ '██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝ ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗',
440
+ '██║ ██║███████║██╔██╗ ██║█████╔╝ ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝',
441
+ '██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗',
442
+ '██████╔╝██║ ██║██║ ╚████║██║ ██╗ ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║',
443
+ '╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝',
444
+ ];
445
+ // Check terminal width — fall back to compact title if too narrow
446
+ const termW = (process.stdout.columns || 120) - 6; // account for box borders
447
+ const useBigTitle = termW >= 92;
448
+ if (useBigTitle) {
449
+ for (let i = 0; i < titleLines.length; i++) {
450
+ const t = i / (titleLines.length - 1);
451
+ const from = t < 0.5
452
+ ? [lerp(192, 139, t * 2), lerp(132, 92, t * 2), lerp(252, 246, t * 2)]
453
+ : [lerp(139, 34, (t - 0.5) * 2), lerp(92, 211, (t - 0.5) * 2), lerp(246, 238, (t - 0.5) * 2)];
454
+ lines.push(bRow(` ${c.bold}${gradientLine(titleLines[i], from, [52, 211, 153])}${c.reset}`));
455
+ }
456
+ } else {
457
+ const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
458
+ lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
459
+ }
460
+
461
+ lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
437
462
 
438
463
  // Subtitle info
439
464
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
@@ -597,6 +622,18 @@ function renderDashboard() {
597
622
  balStr = `${D}⏣${' '.repeat(colBal - 3)}-${c.reset}`;
598
623
  }
599
624
 
625
+ // ── Lifesaver indicator ──
626
+ const ls = wk._lifesavers;
627
+ let lsStr;
628
+ if (ls === 0) lsStr = `${R}♥0${c.reset}`;
629
+ else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${c.reset}`;
630
+ else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
631
+ else lsStr = `${D}♥?${c.reset}`;
632
+
633
+ // ── Level indicator ──
634
+ const lvl = wk._level || 0;
635
+ const lvlStr = lvl > 0 ? `${Cy}L${lvl}${c.reset}` : `${D}L?${c.reset}`;
636
+
600
637
  // ── Earned (fixed visible width) ──
601
638
  const earnNum = wk.stats.coins || 0;
602
639
  let earnStr;
@@ -612,7 +649,7 @@ function renderDashboard() {
612
649
  const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
613
650
  const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
614
651
 
615
- lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${earnStr} ${earnBar} ${actLabel}`));
652
+ lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lvlStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
616
653
  }
617
654
 
618
655
  // Overflow summary
@@ -1400,6 +1437,24 @@ class AccountWorker {
1400
1437
  // Final result on same line
1401
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}`;
1402
1439
  process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1440
+
1441
+ // Extract lifesaver count from inventory and cache in Redis
1442
+ if (result.items && redis) {
1443
+ const lsItem = result.items.find(i =>
1444
+ /life\s*saver/i.test(i.name) || /lifesaver/i.test(i.name)
1445
+ );
1446
+ const lsCount = lsItem ? lsItem.qty : 0;
1447
+ this._lifesavers = lsCount;
1448
+ try {
1449
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
1450
+ if (lsCount === 0) {
1451
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1452
+ } else {
1453
+ await redis.del(`raw:alert:no-lifesaver:${this.channel?.id}`);
1454
+ }
1455
+ } catch {}
1456
+ }
1457
+
1403
1458
  try {
1404
1459
  await fetch(`${API_URL}/api/grinder/inventory`, {
1405
1460
  method: 'POST',
@@ -1554,6 +1609,28 @@ class AccountWorker {
1554
1609
  }
1555
1610
  }
1556
1611
 
1612
+ // Raw logger fallback — CV2 text is captured directly from gateway
1613
+ if (!text || !looksLikeBalance(text)) {
1614
+ const rawData = rawLogger.getLastRaw(this.channel?.id);
1615
+ if (rawData && rawData.cv2Text) {
1616
+ const rawText = rawData.cv2Text;
1617
+ if (looksLikeBalance(rawText)) {
1618
+ text = rawText;
1619
+ this.log('debug', 'Balance: using rawLogger CV2 text fallback');
1620
+ }
1621
+ }
1622
+ // Also try from Redis raw message
1623
+ if ((!text || !looksLikeBalance(text)) && response?.id) {
1624
+ try {
1625
+ const rawMsg = await rawLogger.getMsg(response.id);
1626
+ if (rawMsg?.allText && looksLikeBalance(rawMsg.allText)) {
1627
+ text = rawMsg.allText;
1628
+ this.log('debug', 'Balance: using rawLogger Redis fallback');
1629
+ }
1630
+ } catch {}
1631
+ }
1632
+ }
1633
+
1557
1634
  if (!text) {
1558
1635
  this.log('warn', 'Balance response was empty after waiting for update');
1559
1636
  return;
@@ -1629,12 +1706,80 @@ class AccountWorker {
1629
1706
  balance: wallet,
1630
1707
  bank_balance: bank,
1631
1708
  total_balance: wallet + bank,
1709
+ lifesavers: this._lifesavers ?? null,
1632
1710
  }),
1633
1711
  });
1634
1712
  } catch { /* silent */ }
1635
1713
  }
1636
1714
  }
1637
1715
 
1716
+ // ── Check DM History for deaths/level-ups ──────────────────
1717
+ async checkDmHistory() {
1718
+ try {
1719
+ const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
1720
+ const dm = await dankUser.createDM();
1721
+ this._dmChannelId = dm.id;
1722
+ const recent = await dm.messages.fetch({ limit: 20 });
1723
+
1724
+ let deaths = 0, levelUps = 0, currentLevel = 0, lastLifesaverCount = -1;
1725
+ for (const [, msg] of recent) {
1726
+ const text = stripAnsi(getFullText(msg)).toLowerCase();
1727
+
1728
+ // Death detection
1729
+ if (text.includes('you died') || text.includes('lifesaver protected')) {
1730
+ deaths++;
1731
+ // Button label: "You have 0 Life Saver left" or "You have 3 Life Saver left"
1732
+ for (const row of (msg.components || [])) {
1733
+ for (const comp of (row.components || [row])) {
1734
+ const label = (comp.label || '').toLowerCase();
1735
+ const lsMatch = label.match(/you have (\d+) life\s*saver/i);
1736
+ if (lsMatch && lastLifesaverCount === -1) {
1737
+ lastLifesaverCount = parseInt(lsMatch[1]);
1738
+ }
1739
+ }
1740
+ }
1741
+ // Fallback: check text
1742
+ if (lastLifesaverCount === -1) {
1743
+ const lsMatch = text.match(/(\d+)\s*life\s*saver/i);
1744
+ if (lsMatch) lastLifesaverCount = parseInt(lsMatch[1]);
1745
+ }
1746
+ }
1747
+
1748
+ // Level up detection
1749
+ if (text.includes('leveled up') || text.includes('level up')) {
1750
+ levelUps++;
1751
+ const m = text.match(/level\s+\*{0,2}(\d+)\*{0,2}\s+to\s+\*{0,2}(\d+)\*{0,2}/i);
1752
+ if (m && parseInt(m[2]) > currentLevel) {
1753
+ currentLevel = parseInt(m[2]);
1754
+ }
1755
+ }
1756
+ }
1757
+
1758
+ // Update Redis with findings
1759
+ if (redis) {
1760
+ if (currentLevel > 0) {
1761
+ await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1762
+ this._level = currentLevel;
1763
+ this.log('info', `DM level: ${c.bold}${currentLevel}${c.reset}`);
1764
+ }
1765
+ if (lastLifesaverCount >= 0) {
1766
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
1767
+ this._lifesavers = lastLifesaverCount;
1768
+ if (lastLifesaverCount === 0) {
1769
+ await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1770
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1771
+ this.log('error', `${c.red}0 LIFESAVERS! Crime/Search will be disabled.${c.reset}`);
1772
+ }
1773
+ }
1774
+ }
1775
+
1776
+ return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1777
+ } catch (e) {
1778
+ this.log('debug', `DM check failed: ${e.message}`);
1779
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1780
+ }
1781
+ }
1782
+
1638
1783
  // ── Run Single Command ──────────────────────────────────────
1639
1784
  // Each modular command handler sends the command, waits for response,
1640
1785
  // handles Hold Tight / cooldowns / item-buying internally.
@@ -1660,6 +1805,27 @@ class AccountWorker {
1660
1805
  if (shutdownCalled || !this.running) return;
1661
1806
  this.stats.commands++;
1662
1807
 
1808
+ // ── Lifesaver protection: skip crime/search if 0 lifesavers ──
1809
+ if (cmdName === 'crime' || cmdName === 'search') {
1810
+ const noLifesaver = await rawLogger.hasNoLifesaverAlert(this.channel?.id);
1811
+ if (noLifesaver) {
1812
+ this.log('warn', `[${cmdName}] SKIPPED — no lifesavers! (death detected in DMs)`);
1813
+ await this.setCooldown(cmdName, 3600); // block for 1 hour
1814
+ return;
1815
+ }
1816
+ // Also check Redis key for lifesaver count
1817
+ if (redis) {
1818
+ try {
1819
+ const lsCount = await redis.get(`dkg:lifesavers:${this.account.id}`);
1820
+ if (lsCount === '0') {
1821
+ this.log('warn', `[${cmdName}] SKIPPED — 0 lifesavers cached`);
1822
+ await this.setCooldown(cmdName, 3600);
1823
+ return;
1824
+ }
1825
+ } catch {}
1826
+ }
1827
+ }
1828
+
1663
1829
  const cmdOpts = {
1664
1830
  channel: this.channel,
1665
1831
  waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
@@ -1763,6 +1929,42 @@ class AccountWorker {
1763
1929
  return;
1764
1930
  }
1765
1931
 
1932
+ // ── Death / lifesaver detection in command responses ──
1933
+ if (resultLower.includes('you died') || resultLower.includes('lifesaver protected')) {
1934
+ this.log('error', `DEATH DETECTED during ${cmdName}!`);
1935
+ // Check for lifesaver count in the response
1936
+ const lsMatch = result.match(/(\d+)\s*life\s*saver/i);
1937
+ const lsCount = lsMatch ? parseInt(lsMatch[1]) : -1;
1938
+ if (redis) {
1939
+ if (lsCount === 0) {
1940
+ // 0 lifesavers — DISABLE crime/search immediately
1941
+ await redis.set(`dkg:lifesavers:${this.account.id}`, '0', 'EX', 86400);
1942
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1943
+ this.log('error', `0 LIFESAVERS! Disabling crime/search for this account!`);
1944
+ await this.setCooldown('crime', 86400); // 24h
1945
+ await this.setCooldown('search', 86400);
1946
+ sendWebhook('DEATH ALERT', `**${this.username}** died during \`${cmdName}\`!\n**0 lifesavers remaining!**\nCrime/search disabled for 24h.`, 0xef4444);
1947
+ } else if (lsCount > 0) {
1948
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
1949
+ this.log('warn', `Lifesaver used! ${lsCount} remaining.`);
1950
+ if (lsCount <= 2) {
1951
+ sendWebhook('LOW LIFESAVERS', `**${this.username}** has only **${lsCount}** lifesaver(s) left!`, 0xfbbf24);
1952
+ }
1953
+ }
1954
+ }
1955
+ return;
1956
+ }
1957
+
1958
+ // Died flag from crime/search handler (death detected in the command response)
1959
+ if (cmdResult.died) {
1960
+ this.log('error', `${cmdName} → DIED! Checking lifesaver count...`);
1961
+ // The DM will come separately with the actual death details
1962
+ // For now, be cautious — set a short cooldown and let DM listener handle the rest
1963
+ await this.setCooldown('crime', 300); // 5 min cooldown to check DMs
1964
+ await this.setCooldown('search', 300);
1965
+ return;
1966
+ }
1967
+
1766
1968
  // Premium-only command detection — disable for 24h
1767
1969
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
1768
1970
  resultLower.includes('buy the ability to use this command')) {
@@ -1966,7 +2168,7 @@ class AccountWorker {
1966
2168
  // Interactive — response-driven CD (handler sets nextCooldownSec)
1967
2169
  { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
1968
2170
  { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
1969
- { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 10, priority: 3 },
2171
+ // scratch removed requires voting which can't be automated
1970
2172
  { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
1971
2173
  // Time-gated (run ASAP when available)
1972
2174
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
@@ -2101,6 +2303,8 @@ class AccountWorker {
2101
2303
 
2102
2304
  // Set up error/disconnect handlers for auto-recovery
2103
2305
  this._attachRecoveryListeners();
2306
+ rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
2307
+ rawLogger.attachDmLogger(this.client);
2104
2308
 
2105
2309
  await this.client.login(this.account.discord_token);
2106
2310
  this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
@@ -2554,6 +2758,11 @@ class AccountWorker {
2554
2758
  clearTimeout(timeoutId);
2555
2759
  this.username = this.client.user.tag || this.username;
2556
2760
  this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
2761
+
2762
+ // Attach raw gateway logger for CV2 component capture
2763
+ rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
2764
+ rawLogger.attachDmLogger(this.client);
2765
+
2557
2766
  // Report status non-blocking
2558
2767
  fetch(`${API_URL}/api/grinder/status`, {
2559
2768
  method: 'POST',
@@ -2795,6 +3004,27 @@ async function start(apiKey, apiUrl) {
2795
3004
  const checks = [];
2796
3005
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2797
3006
  if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
3007
+
3008
+ // Init rawLogger Redis (uses same URL — logs all raw gateway data)
3009
+ if (REDIS_URL) {
3010
+ rawLogger.init(REDIS_URL).catch(() => {});
3011
+ // Listen for DM death events across all accounts
3012
+ rawLogger.onDmEvent((event, raw) => {
3013
+ if (event.type === 'death' && event.lifesaversLeft === 0) {
3014
+ const channelId = raw.channel_id;
3015
+ // Find which worker uses this DM channel and disable their crime/search
3016
+ for (const w of workers) {
3017
+ if (w.client?.user?.dmChannel?.id === channelId || w.channel?.id) {
3018
+ w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
3019
+ w.setCooldown?.('crime', 86400);
3020
+ w.setCooldown?.('search', 86400);
3021
+ }
3022
+ }
3023
+ sendWebhook?.('DEATH ALERT (DM)', `Account died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
3024
+ }
3025
+ });
3026
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
3027
+ }
2798
3028
  if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
2799
3029
  if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
2800
3030
  if (CLUSTER_ENABLED) {
@@ -2934,6 +3164,73 @@ async function start(apiKey, apiUrl) {
2934
3164
 
2935
3165
  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}`);
2936
3166
  console.log('');
3167
+
3168
+ // Phase 2.5: Check balance for ALL accounts sequentially (CV2 needs raw logger timing)
3169
+ 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}`);
3170
+
3171
+ let balDone = 0;
3172
+ const balProgressInterval = setInterval(() => {
3173
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3174
+ 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}${total}${c.reset} `);
3175
+ }, 80);
3176
+
3177
+ // Run sequentially — parallel causes CV2 text to be empty (raw logger timing)
3178
+ for (const w of activeWorkers) {
3179
+ try {
3180
+ await w.checkBalance();
3181
+ } catch {}
3182
+ balDone++;
3183
+ }
3184
+
3185
+ clearInterval(balProgressInterval);
3186
+ process.stdout.write(`\r${c.clearLine}`);
3187
+
3188
+ // Show balance + lifesaver summary for each account
3189
+ let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3190
+ for (const w of activeWorkers) {
3191
+ const wallet = w.stats?.balance || 0;
3192
+ const bank = w.stats?.bankBalance || 0;
3193
+ const ls = w._lifesavers ?? '?';
3194
+ totalWallet += wallet;
3195
+ totalBank += bank;
3196
+ const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3197
+ 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}`);
3198
+ if (ls === 0) noLifesaverAccounts.push(w.username);
3199
+ }
3200
+ 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}`);
3201
+
3202
+ if (noLifesaverAccounts.length > 0) {
3203
+ console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}WARNING: ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS!${c.reset} Crime/Search disabled for: ${noLifesaverAccounts.join(', ')}`);
3204
+ }
3205
+ console.log('');
3206
+
3207
+ // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3208
+ console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3209
+ let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
3210
+ for (const w of activeWorkers) {
3211
+ try {
3212
+ const dm = await w.checkDmHistory();
3213
+ if (dm.deaths > 0) dmDeaths += dm.deaths;
3214
+ if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3215
+ if (dm.lifesavers === 0) dmNoLs.push(w.username);
3216
+ const parts = [];
3217
+ if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
3218
+ if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
3219
+ if (dm.lifesavers >= 0) {
3220
+ const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3221
+ parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
3222
+ }
3223
+ if (parts.length > 0) {
3224
+ console.log(` ${c.dim}├${c.reset} ${c.bold}${w.username}${c.reset} ${parts.join(' ')}`);
3225
+ }
3226
+ } catch {}
3227
+ }
3228
+ if (dmNoLs.length > 0) {
3229
+ console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}DM confirms 0 lifesavers:${c.reset} ${dmNoLs.join(', ')}`);
3230
+ }
3231
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}DM check${c.reset} ${c.dim}${dmDeaths} deaths, ${dmLevelUps} level-ups found${c.reset}`);
3232
+ console.log('');
3233
+
2937
3234
  console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
2938
3235
  console.log('');
2939
3236