dankgrinder 6.45.0 → 7.1.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.
@@ -1,20 +1,17 @@
1
1
  /**
2
2
  * Beg command handler.
3
3
  * Simple command: send "pls beg", parse coins from response.
4
- * Detects: positive coins, Life Saver drops, negative (no coins).
5
4
  */
6
5
 
7
6
  const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep } = require('./utils');
8
7
 
9
8
  const RE_NEWLINE = /\n/g;
10
- const RE_LIFE_SAVER = /life\s*saver/i;
11
- const RE_BEG_COINS = /you\s+received\s*:\s*[\s\S]{0,200}?([\d,]+)/i;
12
9
 
13
10
  /**
14
11
  * @param {object} opts
15
12
  * @param {object} opts.channel
16
13
  * @param {function} opts.waitForDankMemer
17
- * @returns {Promise<{result: string, coins: number, lifeSaver?: boolean}>}
14
+ * @returns {Promise<{result: string, coins: number}>}
18
15
  */
19
16
  async function runBeg({ channel, waitForDankMemer }) {
20
17
  LOG.cmd(`${c.white}${c.bold}pls beg${c.reset}`);
@@ -36,30 +33,11 @@ async function runBeg({ channel, waitForDankMemer }) {
36
33
 
37
34
  logMsg(response, 'beg');
38
35
  const text = getFullText(response);
39
-
40
- // Extract coins: prefer "You received" pattern for beg responses
41
- let coins = 0;
42
- const begMatch = text.match(RE_BEG_COINS);
43
- if (begMatch) {
44
- coins = parseInt(begMatch[1].replace(/,/g, ''), 10) || 0;
45
- }
46
- // Fallback to general parseCoins
47
- if (coins === 0) {
48
- coins = parseCoins(text);
49
- }
50
-
51
- // Detect Life Saver drops
52
- const lifeSaver = RE_LIFE_SAVER.test(text);
36
+ const coins = parseCoins(text);
53
37
 
54
38
  if (coins > 0) {
55
- const lsNote = lifeSaver ? ` + ${c.cyan}Life Saver${c.reset}` : '';
56
- LOG.coin(`[beg] ${c.green}+⏣ ${coins.toLocaleString()}${lsNote}${c.reset}`);
57
- return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${lifeSaver ? ' + Life Saver' : ''}${c.reset}`, coins, lifeSaver };
58
- }
59
-
60
- if (lifeSaver) {
61
- LOG.coin(`[beg] ${c.cyan}Life Saver${c.reset} (no coins)`);
62
- return { result: 'beg → Life Saver', coins: 0, lifeSaver };
39
+ LOG.coin(`[beg] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
40
+ return { result: `beg ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`, coins };
63
41
  }
64
42
 
65
43
  LOG.info(`[beg] ${text.substring(0, 80).replace(RE_NEWLINE, ' ')}`);
@@ -11,27 +11,10 @@ const sharp = require('sharp');
11
11
  const https = require('https');
12
12
  const http = require('http');
13
13
 
14
- // Allowed CDN hostnames for image downloads (prevents SSRF).
15
- const ALLOWED_IMAGE_HOSTS = new Set([
16
- 'cdn.discordapp.com',
17
- 'media.discordapp.net',
18
- 'images.discordapp.net',
19
- ]);
20
-
21
- const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB cap — prevents memory exhaustion
22
-
23
14
  /**
24
15
  * Download an image from a URL and return as Buffer.
25
- * Only allows Discord CDN URLs and enforces a max size limit.
26
16
  */
27
17
  function downloadImage(url) {
28
- // ── SSRF check ──────────────────────────────────────────────
29
- let hostname;
30
- try { hostname = new URL(url).hostname; } catch { return Promise.reject(new Error('invalid URL')); }
31
- if (!ALLOWED_IMAGE_HOSTS.has(hostname)) {
32
- return Promise.reject(new Error(`disallowed host: ${hostname}`));
33
- }
34
-
35
18
  return new Promise((resolve, reject) => {
36
19
  const proto = url.startsWith('https') ? https : http;
37
20
  const req = proto.get(url, res => {
@@ -39,16 +22,7 @@ function downloadImage(url) {
39
22
  return downloadImage(res.headers.location).then(resolve, reject);
40
23
  }
41
24
  const chunks = [];
42
- let bytesReceived = 0;
43
- res.on('data', c => {
44
- bytesReceived += c.length;
45
- if (bytesReceived > MAX_IMAGE_BYTES) {
46
- req.destroy();
47
- reject(new Error(`image too large: ${bytesReceived} bytes (max ${MAX_IMAGE_BYTES})`));
48
- return;
49
- }
50
- chunks.push(c);
51
- });
25
+ res.on('data', c => chunks.push(c));
52
26
  res.on('end', () => resolve(Buffer.concat(chunks)));
53
27
  res.on('error', reject);
54
28
  });
@@ -13,6 +13,7 @@ const { runHunt } = require('./hunt');
13
13
  const { runDig } = require('./dig');
14
14
  const { runFish, sellAllFish } = require('./fish');
15
15
  const { runPostMemes } = require('./postmemes');
16
+ const { runScratch } = require('./scratch');
16
17
  const { runBlackjack } = require('./blackjack');
17
18
  const { runTrivia, triviaDB } = require('./trivia');
18
19
  const { runWorkShift } = require('./work');
@@ -38,6 +39,7 @@ module.exports = {
38
39
  runFish,
39
40
  sellAllFish,
40
41
  runPostMemes,
42
+ runScratch,
41
43
  runBlackjack,
42
44
  runTrivia,
43
45
  runWorkShift,
@@ -198,7 +198,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
198
198
  LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
199
199
 
200
200
  await channel.send('pls inv');
201
- let response = await waitForDankMemer(15000);
201
+ let response = await waitForDankMemer(10000);
202
202
 
203
203
  if (!response) {
204
204
  LOG.warn('[inv] No response');
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Scratch command handler.
3
+ * Send "pls scratch", click through scratch card buttons.
4
+ * Requires level 25 — checks profile level before running.
5
+ */
6
+
7
+ const {
8
+ LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
9
+ logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
10
+ } = require('./utils');
11
+ const { meetsLevelRequirement } = require('./profile');
12
+
13
+ /**
14
+ * @param {object} opts
15
+ * @param {object} opts.channel
16
+ * @param {function} opts.waitForDankMemer
17
+ * @param {string} [opts.accountId]
18
+ * @param {object} [opts.redis]
19
+ * @returns {Promise<{result: string, coins: number}>}
20
+ */
21
+ async function runScratch({ channel, waitForDankMemer, accountId, redis }) {
22
+ // Check level 25 requirement before wasting a command
23
+ const canRun = await meetsLevelRequirement({ channel, waitForDankMemer, accountId, redis }, 25);
24
+ if (!canRun) {
25
+ LOG.warn(`[scratch] Skipped — need level 25`);
26
+ return { result: 'skipped (need level 25)', coins: 0, skipReason: 'level' };
27
+ }
28
+ LOG.cmd(`${c.white}${c.bold}pls scratch${c.reset}`);
29
+
30
+ await channel.send('pls scratch');
31
+ const response = await waitForDankMemer(10000);
32
+
33
+ if (!response) {
34
+ LOG.warn('[scratch] No response');
35
+ return { result: 'no response', coins: 0 };
36
+ }
37
+
38
+ if (isHoldTight(response)) {
39
+ const reason = getHoldTightReason(response);
40
+ LOG.warn(`[scratch] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
41
+ await sleep(30000);
42
+ return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
43
+ }
44
+
45
+ logMsg(response, 'scratch');
46
+ const buttons = getAllButtons(response);
47
+
48
+ if (buttons.length === 0) {
49
+ const text = getFullText(response);
50
+ const coins = parseCoins(text);
51
+ if (coins > 0) {
52
+ LOG.coin(`[scratch] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
53
+ return { result: `scratch → +⏣ ${coins.toLocaleString()}`, coins };
54
+ }
55
+ return { result: text.substring(0, 60) || 'done', coins: 0 };
56
+ }
57
+
58
+ // Click through all scratch card cells
59
+ let lastResponse = response;
60
+ let clickCount = 0;
61
+ for (let i = 0; i < Math.min(buttons.length, 9); i++) {
62
+ const btn = buttons[i];
63
+ if (btn && !btn.disabled) {
64
+ await humanDelay(300, 700);
65
+ try {
66
+ const followUp = await safeClickButton(lastResponse, btn);
67
+ if (followUp) { lastResponse = followUp; clickCount++; }
68
+ } catch { break; }
69
+ }
70
+ }
71
+
72
+ const finalText = getFullText(lastResponse);
73
+ const coins = parseCoins(finalText);
74
+
75
+ if (coins > 0) {
76
+ LOG.coin(`[scratch] ${clickCount} clicks → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
77
+ return { result: `scratch (${clickCount} clicks) → +⏣ ${coins.toLocaleString()}`, coins };
78
+ }
79
+
80
+ return { result: `scratch done (${clickCount} clicks)`, coins: 0 };
81
+ }
82
+
83
+ module.exports = { runScratch };
@@ -558,8 +558,7 @@ async function clickCV2Button(msg, customId) {
558
558
  const gId = msg.guildId || msg.guild?.id;
559
559
  if (!token) throw new Error('No token for CV2 click');
560
560
  const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
561
- const { randomUUID } = require('crypto');
562
- const nonce = randomUUID();
561
+ const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
563
562
  const payloadObj = {
564
563
  type: 3,
565
564
  application_id: String(msg.applicationId || DANK_MEMER_ID),
@@ -604,8 +603,7 @@ async function clickCV2SelectMenu(msg, customId, values = []) {
604
603
  const gId = msg.guildId || msg.guild?.id;
605
604
  if (!token) throw new Error('No token for CV2 select');
606
605
  const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
607
- const { randomUUID } = require('crypto');
608
- const nonce = randomUUID();
606
+ const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
609
607
  const payloadObj = {
610
608
  type: 3,
611
609
  application_id: String(msg.applicationId || DANK_MEMER_ID),
package/lib/grinder.js CHANGED
@@ -5,7 +5,7 @@ const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/
5
5
  const rawLogger = require('./rawLogger');
6
6
  const {
7
7
  BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
8
- AhoCorasick, LRUCache, StringPool, AsyncBatchQueue,
8
+ AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
9
9
  } = require('./structures');
10
10
  const PKG_VERSION = require('../package.json').version;
11
11
 
@@ -279,9 +279,9 @@ function boxTop(w, color) { return color + BOX.dtl + BOX.dh.repeat(w - 2) + BOX.
279
279
  function boxMid(w, color) { return color + BOX.tee + BOX.h.repeat(w - 2) + BOX.teeR + c.reset; }
280
280
  function boxBot(w, color) { return color + BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr + c.reset; }
281
281
  function boxLine(content, w, color) {
282
- const stripped = stripAnsi(content);
282
+ const stripped = content.replace(/\x1b\[[0-9;]*m/g, '');
283
283
  const pad = Math.max(0, w - 4 - stripped.length);
284
- return color + BOX.dv + c.reset + ' ' + stripAnsi(content) + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
284
+ return color + BOX.dv + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
285
285
  }
286
286
  function thinLine(w) { return ' ' + c.dim + BOX.h.repeat(w - 4) + c.reset; }
287
287
 
@@ -868,8 +868,8 @@ const CMD_NAMES_CLEAN = {
868
868
  bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
869
869
  ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
870
870
  hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
871
- tidy: 'Tidy', farm: 'Farm', daily: 'Daily', monthly: 'Monthly',
872
- adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
871
+ tidy: 'Tidy', farm: 'Farm', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly',
872
+ scratch: 'Scratch', adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
873
873
  drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
874
874
  work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
875
875
  };
@@ -1490,9 +1490,7 @@ class AccountWorker {
1490
1490
  totalValue: result.totalValue || 0,
1491
1491
  }),
1492
1492
  });
1493
- } catch (err) {
1494
- console.error(`[${this.username}] inventory API error:`, err?.message || err);
1495
- }
1493
+ } catch {}
1496
1494
 
1497
1495
  return {
1498
1496
  ok: true,
@@ -1569,9 +1567,7 @@ class AccountWorker {
1569
1567
  raw_text: result.rawText || '',
1570
1568
  }),
1571
1569
  });
1572
- } catch (err) {
1573
- console.error(`[${this.username}] profile API error:`, err?.message || err);
1574
- }
1570
+ } catch {}
1575
1571
 
1576
1572
  return { ok: true, ...result };
1577
1573
  } catch (e) {
@@ -1780,48 +1776,22 @@ class AccountWorker {
1780
1776
  // Each modular command handler sends the command, waits for response,
1781
1777
  // handles Hold Tight / cooldowns / item-buying internally.
1782
1778
  async runCommand(cmdName, prefix) {
1783
- // Slash commands use '/cmd' without a space between slash and subcommand
1784
- // (e.g. '/dep max', '/work shift'). Legacy prefix uses 'pls cmd'.
1785
- const SLASH_CMD = new Map([
1786
- ['dep max', '/dep max'],
1787
- ['with max', '/with max'],
1788
- ['work shift', '/work shift'],
1789
- ['monthly', '/monthly'],
1790
- ]);
1779
+ let cmdString;
1791
1780
  const bjBet = Math.max(5000, this.account.bet_amount || 5000);
1792
1781
  const gambBet = Math.max(10000, this.account.bet_amount || 10000);
1793
1782
 
1794
- let cmdString;
1795
- if (prefix === '/') {
1796
- const slashVariant = SLASH_CMD.get(cmdName);
1797
- if (slashVariant) {
1798
- cmdString = slashVariant;
1799
- } else if (cmdName === 'blackjack') {
1800
- cmdString = `/bj ${bjBet}`;
1801
- } else if (cmdName === 'cointoss') {
1802
- cmdString = `/cointoss ${gambBet}`;
1803
- } else if (cmdName === 'roulette') {
1804
- cmdString = `/roulette ${gambBet}`;
1805
- } else if (cmdName === 'slots') {
1806
- cmdString = `/slots ${gambBet}`;
1807
- } else if (cmdName === 'snakeeyes') {
1808
- cmdString = `/snakeeyes ${gambBet}`;
1809
- } else {
1810
- cmdString = `/${cmdName}`;
1811
- }
1812
- } else {
1813
- switch (cmdName) {
1814
- case 'dep max': cmdString = `${prefix} dep max`; break;
1815
- case 'with max': cmdString = `${prefix} with max`; break;
1816
- case 'blackjack': cmdString = `${prefix} bj ${bjBet}`; break;
1817
- case 'cointoss': cmdString = `${prefix} cointoss ${gambBet}`; break;
1818
- case 'roulette': cmdString = `${prefix} roulette ${gambBet}`; break;
1819
- case 'slots': cmdString = `${prefix} slots ${gambBet}`; break;
1820
- case 'snakeeyes': cmdString = `${prefix} snakeeyes ${gambBet}`; break;
1821
- case 'work shift': cmdString = `${prefix} work shift`; break;
1822
- case 'monthly': cmdString = `${prefix} monthly`; break;
1823
- default: cmdString = `${prefix} ${cmdName}`;
1824
- }
1783
+ switch (cmdName) {
1784
+ case 'dep max': cmdString = `${prefix} dep max`; break;
1785
+ case 'with max': cmdString = `${prefix} with max`; break;
1786
+ case 'blackjack': cmdString = `${prefix} bj ${bjBet}`; break;
1787
+ case 'cointoss': cmdString = `${prefix} cointoss ${gambBet}`; break;
1788
+ case 'roulette': cmdString = `${prefix} roulette ${gambBet}`; break;
1789
+ case 'slots': cmdString = `${prefix} slots ${gambBet}`; break;
1790
+ case 'snakeeyes': cmdString = `${prefix} snakeeyes ${gambBet}`; break;
1791
+ case 'work shift': cmdString = `${prefix} work shift`; break;
1792
+ case 'weekly': cmdString = `${prefix} weekly`; break;
1793
+ case 'monthly': cmdString = `${prefix} monthly`; break;
1794
+ default: cmdString = `${prefix} ${cmdName}`;
1825
1795
  }
1826
1796
 
1827
1797
  if (shutdownCalled || !this.running) return;
@@ -1919,6 +1889,7 @@ class AccountWorker {
1919
1889
  case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
1920
1890
  case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
1921
1891
  case 'fish': cmdResult = await commands.runFish(cmdOpts); break;
1892
+ case 'scratch': cmdResult = await commands.runScratch(cmdOpts); break;
1922
1893
  case 'adventure': cmdResult = await commands.runAdventure(cmdOpts); break;
1923
1894
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
1924
1895
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
@@ -1973,9 +1944,7 @@ class AccountWorker {
1973
1944
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1974
1945
  body: JSON.stringify({ account_id: this.account.id, active: false }),
1975
1946
  });
1976
- } catch (err) {
1977
- console.error(`[${this.username}] status deactivation API error:`, err?.message || err);
1978
- }
1947
+ } catch {}
1979
1948
  await sendLog(this.username, cmdString, 'VERIFICATION — account deactivated', 'error');
1980
1949
  sendWebhook('CAPTCHA ALERT', `**${this.username}** needs verification!\nCommand: \`${cmdName}\`\nSolve in Discord and re-enable from dashboard.`, 0xef4444);
1981
1950
  return;
@@ -2026,6 +1995,16 @@ class AccountWorker {
2026
1995
  return;
2027
1996
  }
2028
1997
 
1998
+ // Died flag from crime/search handler (death detected in the command response)
1999
+ if (cmdResult.died) {
2000
+ this.log('error', `${cmdName} → DIED! Checking lifesaver count...`);
2001
+ // The DM will come separately with the actual death details
2002
+ // For now, be cautious — set a short cooldown and let DM listener handle the rest
2003
+ await this.setCooldown('crime', 300); // 5 min cooldown to check DMs
2004
+ await this.setCooldown('search', 300);
2005
+ return;
2006
+ }
2007
+
2029
2008
  // Premium-only command detection — disable permanently
2030
2009
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
2031
2010
  resultLower.includes('buy the ability to use this command') ||
@@ -2044,9 +2023,8 @@ class AccountWorker {
2044
2023
  const timeMatch = result.match(/<t:(\d+):R>/);
2045
2024
  let waitSec;
2046
2025
  if (timeMatch) {
2047
- // Discord <t:TS:R> format: :R = relative seconds from NOW (not Unix ms).
2048
- // The captured number IS already the number of seconds to wait.
2049
- waitSec = Math.max(60, parseInt(timeMatch[1]));
2026
+ const nextAvail = parseInt(timeMatch[1]) * 1000;
2027
+ waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
2050
2028
  } else {
2051
2029
  const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
2052
2030
  waitSec = defaultWaits[cmdName] || 86400;
@@ -2226,13 +2204,13 @@ class AccountWorker {
2226
2204
 
2227
2205
  // ── Command Map (shared across ticks, used to build the heap) ──
2228
2206
  // Priority: higher = runs first when multiple commands are ready simultaneously.
2229
- // 10 = time-gated (daily/monthly — never miss),
2207
+ // 10 = time-gated (daily/weekly/monthly — never miss),
2230
2208
  // 8 = financial safety (deposit),
2231
2209
  // 7 = gambling fast-cycle (2-3s CD — run MOST often),
2232
2210
  // 6 = fast grinders (10s CD),
2233
2211
  // 5 = medium grinders (20-40s CD),
2234
2212
  // 4 = resource grinders (hunt/dig — need items),
2235
- // 3 = interactive/long CD (adventure/stream/work),
2213
+ // 3 = interactive/long CD (adventure/stream/work/scratch),
2236
2214
  // 2 = utility (drops/use/tidy)
2237
2215
  static COMMAND_MAP = [
2238
2216
  // Gambling — 2-3s CD, highest frequency
@@ -2243,7 +2221,7 @@ class AccountWorker {
2243
2221
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
2244
2222
  // Fast grinders — 10s CD
2245
2223
  { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
2246
- { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
2224
+ { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
2247
2225
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
2248
2226
  { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
2249
2227
  // Medium grinders — 20-25s CD
@@ -2254,19 +2232,20 @@ class AccountWorker {
2254
2232
  { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 5 },
2255
2233
  // Slow grinders — 40s CD
2256
2234
  { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
2257
- { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
2258
- { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
2235
+ { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
2236
+ { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
2259
2237
  // Interactive — response-driven CD (handler sets nextCooldownSec)
2260
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
2261
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
2262
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
2238
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
2239
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
2240
+ // scratch removed requires voting which can't be automated
2241
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
2263
2242
  // Time-gated (run ASAP when available)
2264
- { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
2265
- // monthly — premium only
2266
- { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
2243
+ { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
2244
+ // weekly removed — premium only, not available for free users
2245
+ { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000,priority: 10 },
2267
2246
  // Financial safety
2268
- { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
2269
- { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
2247
+ { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
2248
+ { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
2270
2249
  // Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
2271
2250
  ].map(Object.freeze);
2272
2251
 
@@ -2543,58 +2522,91 @@ class AccountWorker {
2543
2522
  return;
2544
2523
  }
2545
2524
 
2546
- // ── Scan entire queue for ready commands ─────────────────────
2547
- // Previously we only peeked the top item, missing other commands that
2548
- // were already ready while a slower command was at the top of the heap.
2549
- // Now we drain the queue and separate ready vs. waiting commands.
2550
- // All ready commands fire immediately — no priority, FIFO order.
2551
- const readyItems = [];
2552
- const waitingItems = [];
2553
- while (this.commandQueue.size > 0) {
2554
- const item = this.commandQueue.pop();
2555
- if (!item) break;
2556
- if (item.nextRunAt <= now) {
2557
- readyItems.push(item);
2558
- } else {
2559
- waitingItems.push(item);
2560
- }
2525
+ const top = this.commandQueue.peek();
2526
+ if (top.nextRunAt > now) {
2527
+ const waitMs = Math.min(top.nextRunAt - now, 2000);
2528
+ this.setStatus('cooldown...');
2529
+ this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2530
+ return;
2561
2531
  }
2562
2532
 
2563
- // Re-insert waiting items back into the queue (they still have time to wait)
2564
- for (const item of waitingItems) {
2565
- this.commandQueue.push(item);
2533
+ const item = this.commandQueue.pop();
2534
+ if (!item) {
2535
+ this.tickTimeout = setTimeout(() => this.tick(), 1000);
2536
+ return;
2566
2537
  }
2567
2538
 
2568
- if (readyItems.length === 0) {
2569
- // Nothing ready — wait until the soonest command becomes available.
2570
- let minWaitMs = 5000; // cap at 5s to stay responsive
2571
- for (const item of waitingItems) {
2572
- const diff = item.nextRunAt - now;
2573
- if (diff < minWaitMs) minWaitMs = diff;
2574
- }
2575
- this.setStatus('cooldown...');
2576
- this.tickTimeout = setTimeout(() => this.tick(), Math.max(100, Math.min(minWaitMs, 5000)));
2539
+ const ready = await this.isCooldownReady(item.cmd);
2540
+ if (!ready) {
2541
+ const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
2542
+ item.nextRunAt = now + cd * 1000;
2543
+ if (this.commandQueue) this.commandQueue.push(item);
2544
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
2577
2545
  return;
2578
2546
  }
2579
2547
 
2580
- // FIFO execute commands in the order they became ready.
2581
- // Commands that aren't picked this tick go back into the queue for next tick.
2582
- const item = readyItems[0];
2548
+ // Skip time-gated commands if already claimed (in-memory + Redis)
2549
+ if (item.cmd === 'daily' || item.cmd === 'weekly' || item.cmd === 'monthly' || item.cmd === 'drops') {
2550
+ const memExpiry = this.doneToday.get(item.cmd);
2551
+ if (memExpiry && Date.now() < memExpiry) {
2552
+ item.nextRunAt = memExpiry;
2553
+ if (this.commandQueue) this.commandQueue.push(item);
2554
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
2555
+ return;
2556
+ }
2557
+ if (redis) {
2558
+ try {
2559
+ const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
2560
+ if (done) {
2561
+ const ttlMap = { daily: 86400, weekly: 604800, monthly: 2592000, drops: 86400 };
2562
+ const ttl = ttlMap[item.cmd] || 86400;
2563
+ const expiry = now + ttl * 1000;
2564
+ this.doneToday.set(item.cmd, expiry);
2565
+ item.nextRunAt = expiry;
2566
+ if (this.commandQueue) this.commandQueue.push(item);
2567
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
2568
+ return;
2569
+ }
2570
+ } catch {}
2571
+ }
2572
+ }
2583
2573
 
2584
- // Any remaining ready items that we didn't execute go back immediately
2585
- // so they run in the next tick without waiting for the slow top item.
2586
- for (let i = 1; i < readyItems.length; i++) {
2587
- this.commandQueue.push(readyItems[i]);
2574
+ // Smart gambling: skip gamble commands while loss-paused
2575
+ const GAMBLE_SET = new Set(['blackjack', 'cointoss', 'roulette', 'slots', 'snakeeyes']);
2576
+ if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > now) {
2577
+ item.nextRunAt = this._gamblePausedUntil;
2578
+ if (this.commandQueue) this.commandQueue.push(item);
2579
+ this.setStatus(`gamble paused (${Math.ceil((this._gamblePausedUntil - now) / 1000)}s)`);
2580
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
2581
+ return;
2582
+ }
2583
+ if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > 0 && this._gamblePausedUntil <= now) {
2584
+ this._gamblePausedUntil = 0;
2585
+ this._gambleLossStreak = 0;
2586
+ this._gambleSessionLoss = 0;
2587
+ this.log('info', 'Gambling pause expired — resuming bets');
2588
2588
  }
2589
2589
 
2590
- // Anti-detection: per-account jitter + micro-pauses for this command
2590
+ // TokenBucket rate limiter: prevent Discord 429s by throttling commands
2591
+ if (!this._rateLimiter.consume(1)) {
2592
+ const waitMs = this._rateLimiter.waitTime(1);
2593
+ this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
2594
+ if (this.commandQueue) this.commandQueue.push(item);
2595
+ this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2596
+ return;
2597
+ }
2598
+
2599
+ this._cmdRate.increment();
2600
+ this.busy = true;
2591
2601
  const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
2602
+ // Anti-detection: per-account jitter with varying patterns
2592
2603
  const patternMod = this._activePattern;
2593
2604
  const jitterBase = cd <= 5
2594
2605
  ? 0.3 + Math.random() * 0.7
2595
2606
  : cd <= 20
2596
2607
  ? 0.5 + Math.random() * 1.5
2597
2608
  : 1 + Math.random() * 2;
2609
+ // Add human-like micro-pauses (occasionally take longer, simulating distraction)
2598
2610
  const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
2599
2611
  const totalWait = cd + jitterBase + microPause;
2600
2612
 
@@ -2606,32 +2618,6 @@ class AccountWorker {
2606
2618
  await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
2607
2619
  }
2608
2620
 
2609
- // TokenBucket rate limiter — prevent Discord 429s
2610
- if (!this._rateLimiter.consume(1)) {
2611
- const waitMs = this._rateLimiter.waitTime(1);
2612
- this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
2613
- if (this.commandQueue) this.commandQueue.push(item);
2614
- this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2615
- return;
2616
- }
2617
-
2618
- // Startup delay: don't send commands for the first 30s after grindLoop() starts.
2619
- // This prevents flooding Dank Memer during the Phase 2 inventory check which
2620
- // sends pls inv for all accounts simultaneously after login.
2621
- if (this._startupDelayUntil && now < this._startupDelayUntil) {
2622
- const waitMs = this._startupDelayUntil - now;
2623
- this.setStatus('warming up...');
2624
- item.nextRunAt = now + waitMs + 1000;
2625
- if (this.commandQueue) this.commandQueue.push(item);
2626
- this.tickTimeout = setTimeout(() => this.tick(), waitMs + 1000);
2627
- return;
2628
- }
2629
-
2630
- this.busy = true;
2631
-
2632
- // ── Run command (with interactive retry) ───────────────────
2633
- // Commands run ONE BY ONE — sequential execution, no concurrency within this account.
2634
- // Each runCommand() call waits for Dank Memer's Discord response before returning.
2635
2621
  const prefix = this.account.use_slash ? '/' : 'pls';
2636
2622
  this.setStatus(formatCommandName(item.cmd));
2637
2623
 
@@ -2645,40 +2631,15 @@ class AccountWorker {
2645
2631
  next_run_at: nextItemRun?.nextRunAt || null,
2646
2632
  });
2647
2633
 
2648
- // Interactive commands (button-click): retry up to 3 times on failure.
2649
- // Non-interactive commands run once.
2650
- const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
2651
- const isInteractive = INTERACTIVE_CMDS.has(item.cmd);
2652
- const maxAttempts = isInteractive ? 3 : 1;
2653
- let attempt = 0;
2654
- let lastError = null;
2655
- let earned = 0;
2656
- while (attempt < maxAttempts) {
2657
- attempt++;
2658
- try {
2659
- const beforeCoins = this.stats.coins;
2660
- await this.runCommand(item.cmd, prefix);
2661
- earned = this.stats.coins - beforeCoins;
2662
- lastError = null;
2663
- this.lastCommandRun = Date.now();
2664
- break; // success
2665
- } catch (err) {
2666
- lastError = err;
2667
- this.log('warn', `${item.cmd} attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
2668
- if (attempt < maxAttempts) {
2669
- await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));
2670
- }
2671
- }
2672
- }
2673
- if (lastError) {
2674
- this.log('error', `${item.cmd} failed after ${maxAttempts} attempts — skipping`);
2675
- }
2676
-
2677
- this._cmdRate.increment();
2634
+ const beforeCoins = this.stats.coins;
2635
+ await this.runCommand(item.cmd, prefix);
2636
+ const earned = this.stats.coins - beforeCoins;
2678
2637
 
2679
- // Grace period for interactive commands — Dank Memer needs time to process
2680
- // the interaction before accepting the next command.
2681
- if (isInteractive) {
2638
+ // Grace period for interactive (button-click) commands — Dank Memer
2639
+ // needs time to process the interaction before accepting the next command.
2640
+ // Without this, the next command gets "Hold Tight" errors.
2641
+ const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
2642
+ if (INTERACTIVE_CMDS.has(item.cmd)) {
2682
2643
  await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
2683
2644
  }
2684
2645
 
@@ -2696,6 +2657,8 @@ class AccountWorker {
2696
2657
  this.failStreak = 0;
2697
2658
  }
2698
2659
 
2660
+ this.lastCommandRun = Date.now();
2661
+
2699
2662
  // Exponential backoff: if too many consecutive failures, slow down
2700
2663
  const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
2701
2664
  // Minimum 5s cooldown for failed commands to prevent rapid-fire retries
@@ -2767,11 +2730,6 @@ class AccountWorker {
2767
2730
  this.failStreak = 0;
2768
2731
  this.cycleCount = 0;
2769
2732
  this.lastCommandRun = 0;
2770
- // Delay first command by 30s to avoid competing with Phase 2 inventory check
2771
- // which sends pls inv for all accounts simultaneously after login.
2772
- // Without this, the grind loop floods Dank Memer with commands during the
2773
- // login surge, triggering rate-limits that cause Phase 2 inventory to fail.
2774
- this._startupDelayUntil = Date.now() + 30000;
2775
2733
  await this._loadLearnedCooldowns();
2776
2734
  this.commandQueue = await this.buildCommandQueue();
2777
2735
  this.lastHealthCheck = Date.now();
@@ -2875,9 +2833,7 @@ class AccountWorker {
2875
2833
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2876
2834
  body: JSON.stringify({ action_id: action.id }),
2877
2835
  });
2878
- } catch (err) {
2879
- console.error(`[${this.username}] delete pending action error:`, err?.message || err);
2880
- }
2836
+ } catch {}
2881
2837
  }
2882
2838
  if (action.action === 'check_profile' && !this.busy) {
2883
2839
  this.log('info', 'Dashboard requested profile check');
@@ -2888,15 +2844,11 @@ class AccountWorker {
2888
2844
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2889
2845
  body: JSON.stringify({ action_id: action.id }),
2890
2846
  });
2891
- } catch (err) {
2892
- console.error(`[${this.username}] delete pending action error:`, err?.message || err);
2893
- }
2847
+ } catch {}
2894
2848
  }
2895
2849
  }
2896
2850
  }
2897
- } catch (err) {
2898
- console.error(`[${this.username}] refreshConfig error:`, err?.message || err);
2899
- }
2851
+ } catch { /* silent */ }
2900
2852
  }
2901
2853
 
2902
2854
  async start() {
@@ -2946,8 +2898,9 @@ class AccountWorker {
2946
2898
  { key: 'cmd_fish', l: 'fish' }, { key: 'cmd_beg', l: 'beg' },
2947
2899
  { key: 'cmd_search', l: 'search' }, { key: 'cmd_hl', l: 'hl' },
2948
2900
  { key: 'cmd_crime', l: 'crime' }, { key: 'cmd_pm', l: 'pm' },
2949
- { key: 'cmd_daily', l: 'daily' }, { key: 'cmd_monthly', l: 'monthly' },
2950
- { key: 'cmd_work', l: 'work' }, { key: 'cmd_stream', l: 'stream' },
2901
+ { key: 'cmd_daily', l: 'daily' }, { key: 'cmd_weekly', l: 'weekly' },
2902
+ { key: 'cmd_monthly', l: 'monthly' }, { key: 'cmd_work', l: 'work' },
2903
+ { key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
2951
2904
  { key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
2952
2905
  { key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
2953
2906
  { key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
@@ -3172,38 +3125,19 @@ async function start(apiKey, apiUrl) {
3172
3125
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
3173
3126
  if (REDIS_URL) {
3174
3127
  rawLogger.init(REDIS_URL).catch(() => {});
3175
- // Live DM listener: detect deaths and level-ups in real-time across all accounts
3128
+ // Listen for DM death events across all accounts
3176
3129
  rawLogger.onDmEvent((event, raw) => {
3177
- const dmChannelId = raw.channel_id;
3178
-
3179
- // Find which worker owns this DM channel
3180
- const worker = workers.find(w => w._dmChannelId === dmChannelId);
3181
- if (!worker) return;
3182
-
3183
- if (event.type === 'death') {
3184
- const lsLeft = event.lifesaversLeft;
3185
-
3186
- if (lsLeft === 0) {
3187
- // 0 lifesavers — disable crime/search immediately
3188
- worker.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
3189
- worker.setCooldown?.('crime', 86400);
3190
- worker.setCooldown?.('search', 86400);
3191
- worker._lifesavers = 0;
3192
- sendWebhook?.('DEATH ALERT (DM)', `**${worker.username}** died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
3193
- } else if (lsLeft > 0) {
3194
- // Lifesaver(s) used — update count in real-time
3195
- worker._lifesavers = lsLeft;
3196
- worker.log?.('warn', `Lifesaver used! ${lsLeft} remaining.`);
3197
- if (lsLeft <= 2) {
3198
- sendWebhook?.('LOW LIFESAVERS', `**${worker.username}** died! Only **${lsLeft}** lifesaver(s) left!`, 0xfbbf24);
3130
+ if (event.type === 'death' && event.lifesaversLeft === 0) {
3131
+ const channelId = raw.channel_id;
3132
+ // Find which worker uses this DM channel and disable their crime/search
3133
+ for (const w of workers) {
3134
+ if (w.client?.user?.dmChannel?.id === channelId || w.channel?.id) {
3135
+ w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
3136
+ w.setCooldown?.('crime', 86400);
3137
+ w.setCooldown?.('search', 86400);
3199
3138
  }
3200
3139
  }
3201
- } else if (event.type === 'levelup') {
3202
- // Level up — update in-memory level
3203
- if (event.to > 0) {
3204
- worker._level = event.to;
3205
- worker.log?.('info', `Level up! Now level ${event.to}.`);
3206
- }
3140
+ sendWebhook?.('DEATH ALERT (DM)', `Account died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
3207
3141
  }
3208
3142
  });
3209
3143
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
@@ -3411,7 +3345,7 @@ async function start(apiKey, apiUrl) {
3411
3345
  const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3412
3346
  const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3413
3347
  let invRes;
3414
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 5, silent: true }); }
3348
+ try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3415
3349
  catch { invRes = { ok: false }; }
3416
3350
  invPending--;
3417
3351
  const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
package/lib/rawLogger.js CHANGED
@@ -151,26 +151,24 @@ function detectCommand(d) {
151
151
  // Non-gambling CV2
152
152
  if (cv2Text.includes('fishing') || cv2Text.includes('fisherfolk')) return 'fish';
153
153
  if (cv2Text.includes('deposit') || cv2Text.includes('bank account')) return 'deposit';
154
- if (cv2Text.includes('begging') || cv2Text.includes('imagine begging') || cv2Text.includes('wumpus gives you') || (cv2Text.includes('you received') && !cv2Text.includes('search') && !cv2Text.includes('hunt') && !cv2Text.includes('dig'))) return 'beg';
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') || (cv2Text.includes('brought back') && (cv2Text.includes('deer') || cv2Text.includes('wolf') || cv2Text.includes('bear') || cv2Text.includes('boar') || cv2Text.includes('lion') || cv2Text.includes('rabbit') || cv2Text.includes('squirrel') || cv2Text.includes('moose') || cv2Text.includes('bird') || cv2Text.includes('elk') || cv2Text.includes('hunting') || cv2Text.includes('capybara')))) 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';
154
+ if (cv2Text.includes('begging') || cv2Text.includes('imagine begging')) return 'beg';
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
- // Quest completions (before generic daily/weekly)
159
- if (cv2Text.includes('locations') && (cv2Text.includes('inventory locations') || cv2Text.includes('found locations') || cv2Text.includes('completed your') || cv2Text.includes('locations remaining'))) return 'search';
160
158
  if (cv2Text.includes('weekly')) return 'weekly';
161
159
  if (cv2Text.includes('daily')) return 'daily';
162
- // Only match inventory if it looks like an actual inventory display (header format or sellable indicator)
163
- if (cv2Text.includes('inventory') && (cv2Text.includes('###') || cv2Text.includes('sellable') || cv2Text.includes('<:reply:'))) return 'inventory';
160
+ if (cv2Text.includes('inventory')) return 'inventory';
164
161
  if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
165
162
  if (cv2Text.includes('balances') && cv2Text.includes('global rank')) return 'balance';
166
163
 
167
164
  // Check content text (plain message content)
168
165
  const contentText = (d.content || '').toLowerCase();
169
166
  if (contentText.includes('balances') && contentText.includes('global rank')) return 'balance';
170
- if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed') || contentText.includes('imagine going into the woods')) return 'hunt';
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';
171
169
  if (contentText.includes('you ran an ad for') && contentText.includes('received')) return 'stream';
172
170
  if (contentText.includes('you can\'t interact with your stream')) return 'stream';
173
- if (contentText.includes('you dug in the dirt') || (contentText.includes('found nothing while digging') && (contentText.includes('dug') || contentText.includes('dirt')))) return 'dig';
171
+ if (contentText.includes('you dug in the dirt') || contentText.includes('found nothing while digging')) return 'dig';
174
172
 
175
173
  // Check embed text
176
174
  const embedText = extractEmbedText(d.embeds).toLowerCase();
@@ -199,8 +197,8 @@ function detectCommand(d) {
199
197
  return 'search';
200
198
  }
201
199
  // Hunt / dig
202
- 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') || (embedText.includes('brought back') && (embedText.includes('deer') || embedText.includes('wolf') || embedText.includes('bear') || embedText.includes('boar') || embedText.includes('lion') || embedText.includes('rabbit') || embedText.includes('squirrel') || embedText.includes('moose') || embedText.includes('bird') || embedText.includes('elk') || embedText.includes('capybara')))) return 'hunt';
203
- 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';
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';
204
202
  // Work — match both minigame prompt AND completion
205
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';
206
204
  if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
@@ -209,7 +207,7 @@ function detectCommand(d) {
209
207
  // Postmemes
210
208
  if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
211
209
  // Stream
212
- 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';
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';
213
211
  if (embedText.includes('you can\'t interact with your stream') || embedText.includes('stream can last')) return 'stream';
214
212
  // Deposit
215
213
  if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
@@ -230,7 +228,7 @@ function detectCommand(d) {
230
228
  // Farm
231
229
  if (embedText.includes('farm') && (embedText.includes('harvest') || embedText.includes('plant') || embedText.includes('hoe') || embedText.includes('water'))) return 'farm';
232
230
  // Beg
233
- if (embedText.includes('begging') || embedText.includes('wumpus gives you') || embedText.includes('life saver') || embedText.includes('lifesaver')) return 'beg';
231
+ if (embedText.includes('begging')) return 'beg';
234
232
  // Daily/weekly quest
235
233
  if (embedText.includes('daily quest')) return 'daily';
236
234
  // Fish
package/lib/structures.js CHANGED
@@ -443,25 +443,6 @@ class MinHeap {
443
443
 
444
444
  peek() { return this.heap[0]; }
445
445
 
446
- /**
447
- * Remove a specific node from the heap by reference.
448
- * Used by CommandScheduler.unschedule() to cancel a pending scheduled command.
449
- * O(n) — linear scan since we store node references in a Map.
450
- */
451
- remove(node) {
452
- const idx = this.heap.indexOf(node);
453
- if (idx === -1) return false;
454
- const last = this.heap.pop();
455
- if (idx < this.heap.length) {
456
- this.heap[idx] = last;
457
- // Bubble up or sink down depending on where the removed item was
458
- if (this._bubbleUp(idx) === idx) {
459
- this._sinkDown(idx);
460
- }
461
- }
462
- return true;
463
- }
464
-
465
446
  _bubbleUp(i) {
466
447
  while (i > 0) {
467
448
  const parent = (i - 1) >>> 1;
@@ -699,6 +680,31 @@ class AsyncBatchQueue {
699
680
  destroy() { if (this._timer) clearTimeout(this._timer); this.queue.length = 0; }
700
681
  }
701
682
 
683
+ // ═══════════════════════════════════════════════════════════════
684
+ // JitterBackoff – Decorrelated jitter (AWS-style), O(1)
685
+ // Standard exponential backoff causes "thundering herd" when 10K
686
+ // accounts retry simultaneously. Decorrelated jitter spreads them:
687
+ // sleep = min(cap, random_between(base, sleep_prev * 3))
688
+ // This is the recommended strategy from AWS Architecture Blog.
689
+ // ═══════════════════════════════════════════════════════════════
690
+ class JitterBackoff {
691
+ constructor(baseMs = 1000, capMs = 30000) {
692
+ this.baseMs = baseMs;
693
+ this.capMs = capMs;
694
+ this._sleep = baseMs;
695
+ this.attempt = 0;
696
+ }
697
+
698
+ next() {
699
+ this._sleep = Math.min(this.capMs, this.baseMs + Math.random() * (this._sleep * 3 - this.baseMs));
700
+ this.attempt++;
701
+ return this._sleep;
702
+ }
703
+
704
+ reset() { this._sleep = this.baseMs; this.attempt = 0; }
705
+ get current() { return this._sleep; }
706
+ }
707
+
702
708
  module.exports = {
703
709
  BloomFilter,
704
710
  LRUCache,
@@ -715,4 +721,5 @@ module.exports = {
715
721
  ObjectPool,
716
722
  TimerWheel,
717
723
  AsyncBatchQueue,
724
+ JitterBackoff,
718
725
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.45.0",
3
+ "version": "7.1.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"