dankgrinder 6.14.0 → 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
@@ -431,10 +431,34 @@ function renderDashboard() {
431
431
  lines.push(bTop);
432
432
  lines.push(bEmpty);
433
433
 
434
- // Title with animated spinner
435
434
  const spin = getSpinner('braille');
436
- const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
437
- 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}`));
438
462
 
439
463
  // Subtitle info
440
464
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
@@ -606,6 +630,10 @@ function renderDashboard() {
606
630
  else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
607
631
  else lsStr = `${D}♥?${c.reset}`;
608
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
+
609
637
  // ── Earned (fixed visible width) ──
610
638
  const earnNum = wk.stats.coins || 0;
611
639
  let earnStr;
@@ -621,7 +649,7 @@ function renderDashboard() {
621
649
  const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
622
650
  const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
623
651
 
624
- lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
652
+ lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lvlStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
625
653
  }
626
654
 
627
655
  // Overflow summary
@@ -1581,6 +1609,28 @@ class AccountWorker {
1581
1609
  }
1582
1610
  }
1583
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
+
1584
1634
  if (!text) {
1585
1635
  this.log('warn', 'Balance response was empty after waiting for update');
1586
1636
  return;
@@ -1663,6 +1713,73 @@ class AccountWorker {
1663
1713
  }
1664
1714
  }
1665
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
+
1666
1783
  // ── Run Single Command ──────────────────────────────────────
1667
1784
  // Each modular command handler sends the command, waits for response,
1668
1785
  // handles Hold Tight / cooldowns / item-buying internally.
@@ -2051,7 +2168,7 @@ class AccountWorker {
2051
2168
  // Interactive — response-driven CD (handler sets nextCooldownSec)
2052
2169
  { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
2053
2170
  { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
2054
- { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 10, priority: 3 },
2171
+ // scratch removed requires voting which can't be automated
2055
2172
  { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
2056
2173
  // Time-gated (run ASAP when available)
2057
2174
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
@@ -3048,7 +3165,7 @@ async function start(apiKey, apiUrl) {
3048
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}`);
3049
3166
  console.log('');
3050
3167
 
3051
- // Phase 2.5: Check balance for ALL accounts + show lifesaver status
3168
+ // Phase 2.5: Check balance for ALL accounts sequentially (CV2 needs raw logger timing)
3052
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}`);
3053
3170
 
3054
3171
  let balDone = 0;
@@ -3057,12 +3174,13 @@ async function start(apiKey, apiUrl) {
3057
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} `);
3058
3175
  }, 80);
3059
3176
 
3060
- await Promise.all(activeWorkers.map(async (w) => {
3177
+ // Run sequentially — parallel causes CV2 text to be empty (raw logger timing)
3178
+ for (const w of activeWorkers) {
3061
3179
  try {
3062
3180
  await w.checkBalance();
3063
3181
  } catch {}
3064
3182
  balDone++;
3065
- }));
3183
+ }
3066
3184
 
3067
3185
  clearInterval(balProgressInterval);
3068
3186
  process.stdout.write(`\r${c.clearLine}`);
@@ -3086,6 +3204,33 @@ async function start(apiKey, apiUrl) {
3086
3204
  }
3087
3205
  console.log('');
3088
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
+
3089
3234
  console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
3090
3235
  console.log('');
3091
3236
 
package/lib/rawLogger.js CHANGED
@@ -355,8 +355,17 @@ function attachDmLogger(client, opts = {}) {
355
355
  const m = allText.match(/level\s+(\d+)\s+to\s+(\d+)/i);
356
356
  dmEvent = { type: 'levelup', from: m ? parseInt(m[1]) : 0, to: m ? parseInt(m[2]) : 0 };
357
357
  } else if (allText.includes('lifesaver protected') || allText.includes('you died')) {
358
- const ls = allText.match(/(\d+)\s*lifesaver/i);
359
- dmEvent = { type: 'death', lifesaversLeft: ls ? parseInt(ls[1]) : -1 };
358
+ // Parse lifesaver count from button labels: "You have 0 Life Saver left"
359
+ let lsLeft = -1;
360
+ const btnText = extractButtons(d.components).map(b => (b.label || '').toLowerCase()).join(' ');
361
+ const btnMatch = btnText.match(/you have (\d+) life\s*saver/i);
362
+ if (btnMatch) lsLeft = parseInt(btnMatch[1]);
363
+ // Fallback to embed text
364
+ if (lsLeft === -1) {
365
+ const ls = allText.match(/(\d+)\s*life\s*saver/i);
366
+ if (ls) lsLeft = parseInt(ls[1]);
367
+ }
368
+ dmEvent = { type: 'death', lifesaversLeft: lsLeft };
360
369
  } else if (allText.includes('you were robbed') || allText.includes('just robbed you')) {
361
370
  const coins = allText.match(/[⏣]\s*([\d,]+)/);
362
371
  dmEvent = { type: 'robbed', amount: coins ? parseInt(coins[1].replace(/,/g, '')) : 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.14.0",
3
+ "version": "6.16.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"