dankgrinder 5.23.0 → 5.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.
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Advanced Anti-Detection System
3
+ *
4
+ * Features:
5
+ * - Adaptive human-like timing with context-aware delays
6
+ * - Micro-pause injection for natural interaction patterns
7
+ * - Activity-based variance (tired vs alert simulation)
8
+ * - Session behavior drift (mimics human consistency changes)
9
+ * - Randomized click timing within safe bounds
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ // ── Configuration ─────────────────────────────────────────────
15
+ const CONFIG = Object.freeze({
16
+ // Base delay ranges in ms (tuned for natural human timing)
17
+ MIN_DELAY: 500, // 0.5s minimum - natural human speed
18
+ MAX_DELAY: 1000, // 1.0s maximum - not too slow
19
+
20
+ // Context multipliers (subtle variations)
21
+ MULT_FIRST_ACTION: 1.2, // Slight hesitation on first action
22
+ MULT_REPEATED_ACTION: 0.85, // Slightly faster on repeated actions
23
+ MULT_HIGH_VALUE: 1.1, // Marginally slower for high-value actions
24
+ MULT_LOW_VALUE: 0.9, // Slightly faster for low-value actions
25
+
26
+ // Micro-pause settings (subtle human-like hesitation)
27
+ MICRO_PAUSE_CHANCE: 0.25, // 25% chance of micro-pause
28
+ MICRO_PAUSE_MIN: 50,
29
+ MICRO_PAUSE_MAX: 150,
30
+
31
+ // Minimal drift to avoid detectable patterns
32
+ DRIFT_FACTOR: 0.01,
33
+ DRIFT_MAX: 0.08, // Max 8% drift
34
+
35
+ // Pattern randomization
36
+ PATTERN_RESET_CHANCE: 0.2, // 20% chance to reset timing pattern
37
+ });
38
+
39
+ // ── Session State ─────────────────────────────────────────────
40
+ const sessionState = {
41
+ actionCount: 0,
42
+ lastActionTime: 0,
43
+ currentDrift: 0,
44
+ patternSeed: Math.random(),
45
+ activityLevel: 1.0, // Starts neutral
46
+ };
47
+
48
+ // ── Seeded Random for Reproducible Patterns ──────────────────
49
+ function seededRandom(seed) {
50
+ const x = Math.sin(seed) * 10000;
51
+ return x - Math.floor(x);
52
+ }
53
+
54
+ // ── Adaptive Delay Calculation ────────────────────────────────
55
+ /**
56
+ * Calculate human-like delay based on context.
57
+ * @param {Object} options
58
+ * @param {string} options.context - Action context ('first', 'repeated', 'high-value', 'low-value', 'normal')
59
+ * @param {number} options.baseMin - Minimum delay override
60
+ * @param {number} options.baseMax - Maximum delay override
61
+ * @param {boolean} options.skipMicroPause - Force skip micro-pause
62
+ * @returns {Promise<number>} - Delay in ms
63
+ */
64
+ async function calcAdaptiveDelay(options = {}) {
65
+ const {
66
+ context = 'normal',
67
+ baseMin = CONFIG.MIN_DELAY,
68
+ baseMax = CONFIG.MAX_DELAY,
69
+ skipMicroPause = false,
70
+ } = options;
71
+
72
+ sessionState.actionCount++;
73
+ const now = Date.now();
74
+
75
+ // Time since last action affects speed (rush vs relaxed)
76
+ const timeSinceLast = now - sessionState.lastActionTime;
77
+ const rushFactor = timeSinceLast < 500 ? 0.8 : timeSinceLast > 5000 ? 1.15 : 1.0;
78
+
79
+ // Apply context multiplier
80
+ let multiplier = 1.0;
81
+ switch (context) {
82
+ case 'first':
83
+ multiplier = CONFIG.MULT_FIRST_ACTION;
84
+ break;
85
+ case 'repeated':
86
+ multiplier = CONFIG.MULT_REPEATED_ACTION;
87
+ break;
88
+ case 'high-value':
89
+ multiplier = CONFIG.MULT_HIGH_VALUE;
90
+ break;
91
+ case 'low-value':
92
+ multiplier = CONFIG.MULT_LOW_VALUE;
93
+ break;
94
+ }
95
+
96
+ // Apply drift (session behavior change)
97
+ sessionState.currentDrift += (Math.random() - 0.5) * CONFIG.DRIFT_FACTOR;
98
+ sessionState.currentDrift = Math.max(
99
+ -CONFIG.DRIFT_MAX,
100
+ Math.min(CONFIG.DRIFT_MAX, sessionState.currentDrift)
101
+ );
102
+ multiplier *= (1 + sessionState.currentDrift);
103
+
104
+ // Pattern reset for unpredictability
105
+ if (Math.random() < CONFIG.PATTERN_RESET_CHANCE) {
106
+ sessionState.patternSeed = Math.random();
107
+ }
108
+
109
+ // Calculate base delay with seeded randomness
110
+ const seed = sessionState.patternSeed + (sessionState.actionCount * 0.001);
111
+ const randomFactor = 0.5 + seededRandom(seed); // 0.5 to 1.5 range
112
+ const baseDelay = baseMin + (baseMax - baseMin) * randomFactor;
113
+
114
+ // Apply all modifiers
115
+ let finalDelay = baseDelay * multiplier * rushFactor;
116
+
117
+ // Add micro-pause (simulates human hesitation)
118
+ if (!skipMicroPause && Math.random() < CONFIG.MICRO_PAUSE_CHANCE) {
119
+ const microPause = CONFIG.MICRO_PAUSE_MIN +
120
+ (CONFIG.MICRO_PAUSE_MAX - CONFIG.MICRO_PAUSE_MIN) * seededRandom(seed + 0.5);
121
+ finalDelay += microPause;
122
+ }
123
+
124
+ // Clamp to reasonable bounds
125
+ finalDelay = Math.max(50, Math.min(800, finalDelay));
126
+
127
+ sessionState.lastActionTime = Date.now();
128
+
129
+ return Math.round(finalDelay);
130
+ }
131
+
132
+ // ── Main Human Delay Function ─────────────────────────────────
133
+ /**
134
+ * Wait with human-like timing.
135
+ * @param {Object} options - Same as calcAdaptiveDelay
136
+ */
137
+ async function humanDelay(options = {}) {
138
+ const delay = await calcAdaptiveDelay(options);
139
+ return new Promise(resolve => setTimeout(resolve, delay));
140
+ }
141
+
142
+ // ── Reset Session State ───────────────────────────────────────
143
+ function resetSession() {
144
+ sessionState.actionCount = 0;
145
+ sessionState.lastActionTime = 0;
146
+ sessionState.currentDrift = 0;
147
+ sessionState.patternSeed = Math.random();
148
+ sessionState.activityLevel = 1.0;
149
+ }
150
+
151
+ // ── Get Session Stats (for debugging) ─────────────────────────
152
+ function getSessionStats() {
153
+ return {
154
+ actionCount: sessionState.actionCount,
155
+ currentDrift: sessionState.currentDrift,
156
+ timeSinceLast: Date.now() - sessionState.lastActionTime,
157
+ };
158
+ }
159
+
160
+ // ── Smart Cooldown Calculator ─────────────────────────────────
161
+ /**
162
+ * Calculate smart cooldown with safety buffer and variance.
163
+ * @param {number} baseCooldown - Base cooldown in seconds
164
+ * @param {Object} options
165
+ * @param {boolean} options.addBuffer - Add safety buffer
166
+ * @param {boolean} options.addVariance - Add random variance
167
+ * @returns {number} - Adjusted cooldown in seconds
168
+ */
169
+ function calcSmartCooldown(baseCooldown, options = {}) {
170
+ const { addBuffer = true, addVariance = true } = options;
171
+
172
+ let adjusted = baseCooldown;
173
+
174
+ // Add safety buffer
175
+ if (addBuffer) {
176
+ adjusted += CONFIG.COOLDOWN_BUFFER_SEC;
177
+ }
178
+
179
+ // Add small variance (±2%) to avoid detectable patterns
180
+ if (addVariance && baseCooldown > 10) {
181
+ const variance = (Math.random() - 0.5) * 0.04 * baseCooldown;
182
+ adjusted += variance;
183
+ }
184
+
185
+ return Math.max(5, Math.round(adjusted));
186
+ }
187
+
188
+ // ── Exponential Backoff with Jitter ───────────────────────────
189
+ /**
190
+ * Calculate backoff delay for retries.
191
+ * @param {number} attempt - Attempt number (0-indexed)
192
+ * @param {number} baseMs - Base delay in ms
193
+ * @param {number} maxMs - Maximum delay in ms
194
+ * @returns {number} - Delay in ms
195
+ */
196
+ function calcBackoff(attempt, baseMs = 1000, maxMs = 30000) {
197
+ const exponential = baseMs * Math.pow(2, attempt);
198
+ const jitter = exponential * 0.2 * (Math.random() - 0.5);
199
+ return Math.min(maxMs, Math.round(exponential + jitter));
200
+ }
201
+
202
+ // ── Exports ───────────────────────────────────────────────────
203
+ module.exports = {
204
+ humanDelay,
205
+ calcAdaptiveDelay,
206
+ calcSmartCooldown,
207
+ calcBackoff,
208
+ resetSession,
209
+ getSessionStats,
210
+ CONFIG,
211
+ };
@@ -39,8 +39,7 @@ const {
39
39
  safeClickButton, isHoldTight, logMsg,
40
40
  } = require('./utils');
41
41
 
42
- const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/;
43
- const RE_ADVENTURE_AGAIN_LABEL = /adventure again in (\d+)\s*(minute|min|hour|second)/;
42
+ const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/g;
44
43
 
45
44
  // ── Adventure type rotation (cycle through all types each run) ────
46
45
  let lastAdventureIndex = -1;
@@ -270,7 +269,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
270
269
 
271
270
  if (!response) {
272
271
  LOG.warn('[adventure] No response from Dank Memer');
273
- return { result: 'no response', coins: 0, nextCooldownSec: null };
272
+ return { result: 'no response', coins: 0, nextCooldownSec: 180 };
274
273
  }
275
274
 
276
275
  // Check for Hold Tight
@@ -439,33 +438,45 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
439
438
  function buildResult(finalText, coins, interactions, rewards, msg) {
440
439
  let nextCooldownSec = null;
441
440
 
442
- // 1) Best: Unix timestamp <t:UNIX:t> in final text or embed
443
- const allText = msg ? getFullText(msg) : finalText;
444
- const tsMatch = allText.match(RE_DISCORD_TIMESTAMP);
445
- if (tsMatch) {
446
- const unixTarget = parseInt(tsMatch[1]);
447
- const nowUnix = Math.floor(Date.now() / 1000);
448
- nextCooldownSec = Math.max(5, unixTarget - nowUnix);
449
- LOG.info(`[adventure] Next at ${new Date(unixTarget * 1000).toLocaleTimeString()} (${c.yellow}${nextCooldownSec}s${c.reset})`);
450
- }
451
-
452
- // 2) Fallback: "Adventure again in X minutes" button label
453
- if (!nextCooldownSec && msg) {
441
+ // 1) Best: button label after adventure ends (e.g. "Adventure again in 57m")
442
+ if (msg) {
454
443
  const rows = msg.components || [];
455
444
  for (let ri = 0; ri < rows.length; ri++) {
456
445
  const comps = rows[ri].components || [];
457
446
  for (let ci = 0; ci < comps.length; ci++) {
458
- const comp = comps[ci];
459
- const label = (comp.label || '').toLowerCase();
460
- const btnMatch = label.match(RE_ADVENTURE_AGAIN_LABEL);
461
- if (btnMatch) {
462
- nextCooldownSec = parseInt(btnMatch[1]);
463
- const unit = btnMatch[2].toLowerCase();
464
- if (unit.startsWith('min')) nextCooldownSec *= 60;
465
- if (unit.startsWith('hour')) nextCooldownSec *= 3600;
466
- LOG.info(`[adventure] Next in ${c.yellow}${btnMatch[1]} ${btnMatch[2]}s${c.reset} (from button)`);
467
- }
447
+ const label = String(comps[ci]?.label || '').toLowerCase();
448
+ if (!label.includes('adventure') || !label.includes('again')) continue;
449
+ const unitMatch = label.match(/(\d+)\s*(h|hr|hrs|hour|hours|m|min|mins|minute|minutes|s|sec|secs|second|seconds)\b/i);
450
+ if (!unitMatch) continue;
451
+ const n = parseInt(unitMatch[1], 10);
452
+ const u = unitMatch[2].toLowerCase();
453
+ if (!Number.isFinite(n) || n <= 0) continue;
454
+ if (u.startsWith('h')) nextCooldownSec = n * 3600;
455
+ else if (u.startsWith('m')) nextCooldownSec = n * 60;
456
+ else nextCooldownSec = n;
457
+ LOG.info(`[adventure] Next cooldown from button: ${c.yellow}${nextCooldownSec}s${c.reset}`);
458
+ break;
468
459
  }
460
+ if (nextCooldownSec) break;
461
+ }
462
+ }
463
+
464
+ // 2) Fallback: Unix timestamp <t:UNIX:*> in final text or embed
465
+ const allText = msg ? getFullText(msg) : finalText;
466
+ if (!nextCooldownSec) {
467
+ const nowUnix = Math.floor(Date.now() / 1000);
468
+ const tsMatches = Array.from(String(allText || '').matchAll(RE_DISCORD_TIMESTAMP));
469
+ let best = null;
470
+ for (const m of tsMatches) {
471
+ const unixTarget = parseInt(m[1], 10);
472
+ if (!Number.isFinite(unixTarget)) continue;
473
+ const diff = unixTarget - nowUnix;
474
+ if (diff <= 0) continue;
475
+ if (best == null || diff < best) best = diff;
476
+ }
477
+ if (best != null) {
478
+ nextCooldownSec = Math.max(5, best);
479
+ LOG.info(`[adventure] Next cooldown from timestamp: ${c.yellow}${nextCooldownSec}s${c.reset}`);
469
480
  }
470
481
  }
471
482
 
@@ -4,11 +4,88 @@
4
4
  */
5
5
 
6
6
  const {
7
- LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
7
+ LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton, humanDelay,
8
+ logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
8
9
  isCV2, ensureCV2, stripAnsi,
9
10
  } = require('./utils');
10
11
  const { buyItem } = require('./shop');
11
12
 
13
+ const DIG_LANE_LABELS = new Set(['left', 'middle', 'right']);
14
+
15
+ async function waitForDigUpdate(channel, messageId, baselineText, waitForDankMemer, timeoutMs = 9000) {
16
+ const started = Date.now();
17
+ while (Date.now() - started < timeoutMs) {
18
+ const msg = await waitForDankMemer(2200);
19
+ if (msg && msg.id === messageId) {
20
+ if (isCV2(msg)) await ensureCV2(msg, true);
21
+ const txt = stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim();
22
+ if (!baselineText || (txt && txt !== baselineText)) return msg;
23
+ }
24
+
25
+ await sleep(350);
26
+ try {
27
+ const fresh = await channel.messages.fetch(messageId);
28
+ if (!fresh) continue;
29
+ if (isCV2(fresh)) await ensureCV2(fresh, true);
30
+ const txt = stripAnsi(getFullText(fresh)).replace(/\s+/g, ' ').trim();
31
+ if (!baselineText || (txt && txt !== baselineText)) return fresh;
32
+ } catch {}
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function pickDigLaneButton(msg) {
38
+ const buttons = getAllButtons(msg).filter((b) => !b.disabled && DIG_LANE_LABELS.has(String(b.label || '').toLowerCase()));
39
+ if (buttons.length === 0) return null;
40
+
41
+ const textLower = stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim().toLowerCase();
42
+
43
+ // If prompt text explicitly mentions a lane, follow it.
44
+ for (const lane of DIG_LANE_LABELS) {
45
+ if (new RegExp(`\\b${lane}\\b`, 'i').test(textLower)) {
46
+ const matched = buttons.find((b) => String(b.label || '').toLowerCase() === lane);
47
+ if (matched) return matched;
48
+ }
49
+ }
50
+
51
+ // Fallback: random lane (still better than skipping the minigame).
52
+ return buttons[Math.floor(Math.random() * buttons.length)];
53
+ }
54
+
55
+ async function resolveDigFlow({ channel, waitForDankMemer, response }) {
56
+ let current = response;
57
+
58
+ for (let round = 0; round < 3; round++) {
59
+ if (isCV2(current)) await ensureCV2(current, true);
60
+ const laneBtn = pickDigLaneButton(current);
61
+ if (!laneBtn) break;
62
+
63
+ LOG.info(`[dig] Minigame: clicking "${laneBtn.label}"`);
64
+ const baseline = stripAnsi(getFullText(current)).replace(/\s+/g, ' ').trim();
65
+ await humanDelay(110, 240);
66
+
67
+ let clicked = null;
68
+ try { clicked = await safeClickButton(current, laneBtn); } catch {}
69
+ if (clicked) current = clicked;
70
+
71
+ const updated = await waitForDigUpdate(channel, current.id, baseline, waitForDankMemer, 10000);
72
+ if (updated) {
73
+ current = updated;
74
+ logMsg(current, `dig-minigame-${round + 1}`);
75
+ } else {
76
+ break;
77
+ }
78
+ }
79
+
80
+ const cleanText = stripAnsi(getFullText(current)).replace(/\s+/g, ' ').trim();
81
+ const coins = parseCoins(cleanText);
82
+ if (coins > 0) {
83
+ LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
84
+ return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
85
+ }
86
+ return { result: cleanText.substring(0, 90) || 'done', coins: 0 };
87
+ }
88
+
12
89
  /**
13
90
  * @param {object} opts
14
91
  * @param {object} opts.channel
@@ -66,25 +143,13 @@ async function runDig({ channel, waitForDankMemer, client }) {
66
143
  if (r2) {
67
144
  if (isCV2(r2)) await ensureCV2(r2);
68
145
  logMsg(r2, 'dig-retry');
69
- const t2 = getFullText(r2);
70
- const coins = parseCoins(t2);
71
- if (coins > 0) {
72
- LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
73
- return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
74
- }
75
- return { result: stripAnsi(t2).replace(/\s+/g, ' ').trim().substring(0, 60), coins: 0 };
146
+ return await resolveDigFlow({ channel, waitForDankMemer, response: r2 });
76
147
  }
77
148
  }
78
149
  return { result: 'need shovel (buy failed)', coins: 0 };
79
150
  }
80
151
 
81
- const coins = parseCoins(cleanText);
82
- if (coins > 0) {
83
- LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
84
- return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
85
- }
86
-
87
- return { result: cleanText.substring(0, 60) || 'done', coins: 0 };
152
+ return await resolveDigFlow({ channel, waitForDankMemer, response });
88
153
  }
89
154
 
90
155
  module.exports = { runDig };
@@ -62,6 +62,13 @@ function parseFarmGrowReadySec(text) {
62
62
  if (tsNearReady.length > 0) return Math.max(5, Math.min(...tsNearReady));
63
63
 
64
64
  // Fallback textual patterns.
65
+ const willReadyH = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*hour/i);
66
+ if (willReadyH) return Math.max(5, parseInt(willReadyH[1], 10) * 3600);
67
+ const willReadyM = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*minute/i);
68
+ if (willReadyM) return Math.max(5, parseInt(willReadyM[1], 10) * 60);
69
+ const willReadyS = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*second/i);
70
+ if (willReadyS) return Math.max(5, parseInt(willReadyS[1], 10));
71
+
65
72
  const h = clean.match(/ready\s+in\s+(\d+)\s*hour/i);
66
73
  if (h) return Math.max(5, parseInt(h[1], 10) * 3600);
67
74
  const mi = clean.match(/ready\s+in\s+(\d+)\s*minute/i);
@@ -977,7 +984,7 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
977
984
 
978
985
  if (!response) {
979
986
  LOG.warn('[farm] No response');
980
- return { result: 'no response', coins: 0 };
987
+ return { result: 'no response', coins: 0, nextCooldownSec: 90 };
981
988
  }
982
989
 
983
990
  if (isHoldTight(response)) {
@@ -1022,7 +1029,7 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
1022
1029
  LOG.warn('[farm] Subcommand required response detected; retrying with "pls farm view"');
1023
1030
  await channel.send('pls farm view');
1024
1031
  const retry = await waitForDankMemer(12000);
1025
- if (!retry) return { result: 'no response after farm view retry', coins: 0 };
1032
+ if (!retry) return { result: 'no response after farm view retry', coins: 0, nextCooldownSec: 120 };
1026
1033
  response = retry;
1027
1034
  if (isCV2(response)) await ensureCV2(response);
1028
1035
  logMsg(response, 'farm-retry-view');
@@ -1505,7 +1512,11 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
1505
1512
  }
1506
1513
 
1507
1514
  const coins = parseCoins(text);
1508
- let nextCd = parseFarmCooldownSec(text) || 10;
1515
+ let nextCd = parseFarmCooldownSec(text) || 30;
1516
+ const growReadyEnd = parseFarmGrowReadySec(text);
1517
+ if (Number.isFinite(growReadyEnd) && growReadyEnd > 0) {
1518
+ nextCd = Math.max(nextCd, Math.min(6 * 3600, growReadyEnd + 2));
1519
+ }
1509
1520
  const missingEnd = parseMissingFarmItem(text);
1510
1521
  if (missingEnd) {
1511
1522
  LOG.warn(`[farm] Missing ${c.bold}${missingEnd}${c.reset} after action — will retry in 1h`);
@@ -233,6 +233,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
233
233
  allItems.push(...parseInventoryPage(response));
234
234
 
235
235
  let guard = 0;
236
+ let noChangeCount = 0;
236
237
  while (page < total && guard < Math.max(20, total + 6)) {
237
238
  guard++;
238
239
  const buttons = getAllButtons(response);
@@ -312,12 +313,17 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
312
313
  delete response._cv2buttons;
313
314
 
314
315
  let pageChanged = false;
315
- for (let attempt = 0; attempt < 4; attempt++) {
316
- await sleep(attempt === 0 ? 600 : 1200);
316
+ for (let attempt = 0; attempt < 7; attempt++) {
317
+ await sleep(attempt === 0 ? 700 : 1100 + Math.min(attempt * 150, 600));
317
318
  try {
318
- const fresh = await channel.messages.fetch(response.id);
319
+ const fresh = await response.fetch(true);
319
320
  if (fresh) response = fresh;
320
- } catch {}
321
+ } catch {
322
+ try {
323
+ const fresh = await channel.messages.fetch(response.id);
324
+ if (fresh) response = fresh;
325
+ } catch {}
326
+ }
321
327
  if (isCV2(response)) await ensureCV2(response, true);
322
328
  const pageInfo = parsePageInfo(response);
323
329
  if (pageInfo.page > page && !visitedPages.has(pageInfo.page)) {
@@ -337,7 +343,16 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
337
343
  delete response._cv2buttons;
338
344
  }
339
345
 
340
- if (!pageChanged) break;
346
+ if (!pageChanged) {
347
+ noChangeCount++;
348
+ if (noChangeCount < 3) {
349
+ LOG.warn(`[inv] Page stuck at ${page}/${total}; retrying paginator click (${noChangeCount}/2)`);
350
+ await sleep(900 + Math.floor(Math.random() * 500));
351
+ continue;
352
+ }
353
+ break;
354
+ }
355
+ noChangeCount = 0;
341
356
  allItems.push(...parseInventoryPage(response));
342
357
  }
343
358
 
@@ -8,6 +8,7 @@ const { buyItem } = require('./shop');
8
8
  const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
9
9
  const STREAM_ACTION_LABELS = Object.freeze(['run ad', 'read chat', 'collect donations']);
10
10
  const RE_STREAM_INTERACT_MIN = /interact\s+with\s+your\s+stream\s+every\s+`?(\d+)`?\s*minutes?/i;
11
+ const RE_STREAM_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
11
12
 
12
13
  function normalizeLower(text) {
13
14
  return String(text || '')
@@ -123,11 +124,40 @@ function isActionResultText(lowerText) {
123
124
 
124
125
  function parseStreamInteractCooldownSec(text) {
125
126
  const clean = String(stripAnsi(text || '')).replace(/\s+/g, ' ').trim();
127
+
128
+ // Primary: explicit interaction cadence from dashboard text.
126
129
  const mm = clean.match(RE_STREAM_INTERACT_MIN);
127
130
  if (mm) {
128
131
  const mins = parseInt(mm[1], 10);
129
132
  if (Number.isFinite(mins) && mins > 0) return mins * 60;
130
133
  }
134
+
135
+ // Secondary: derive from "last streamed" marker (10m base cooldown).
136
+ const lower = normalizeLower(clean);
137
+ const now = Math.floor(Date.now() / 1000);
138
+ const tsMatches = Array.from(clean.matchAll(RE_STREAM_TS));
139
+ for (const m of tsMatches) {
140
+ const ts = parseInt(m[1], 10);
141
+ if (!Number.isFinite(ts) || ts <= 0) continue;
142
+ const idx = m.index ?? 0;
143
+ const left = lower.slice(Math.max(0, idx - 70), idx);
144
+ if (!left.includes('last streamed')) continue;
145
+ const elapsed = Math.max(0, now - ts);
146
+ const remain = Math.max(5, 600 - elapsed);
147
+ return remain;
148
+ }
149
+
150
+ // Textual fallback: "last streamed X minutes ago".
151
+ const ago = lower.match(/last\s+streamed[^\d]{0,24}(\d+)\s*(hour|hr|hrs|minute|min|mins|second|sec|secs)\s+ago/i);
152
+ if (ago) {
153
+ const n = parseInt(ago[1], 10);
154
+ const unit = ago[2].toLowerCase();
155
+ if (Number.isFinite(n) && n >= 0) {
156
+ const elapsed = unit.startsWith('h') ? n * 3600 : unit.startsWith('m') ? n * 60 : n;
157
+ return Math.max(5, 600 - elapsed);
158
+ }
159
+ }
160
+
131
161
  return 600;
132
162
  }
133
163
 
@@ -171,7 +201,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
171
201
 
172
202
  if (!response) {
173
203
  LOG.warn('[stream] No response');
174
- return { result: 'no response', coins: 0 };
204
+ return { result: 'no response', coins: 0, nextCooldownSec: 120 };
175
205
  }
176
206
 
177
207
  if (isHoldTight(response)) {
@@ -199,14 +229,14 @@ async function runStream({ channel, waitForDankMemer, client }) {
199
229
 
200
230
  for (const item of itemsToBuy) {
201
231
  const bought = await buyItem({ channel, waitForDankMemer, client, itemName: item, quantity: 1 });
202
- if (!bought) return { result: `need ${item} (buy failed)`, coins: 0 };
232
+ if (!bought) return { result: `need ${item} (buy failed)`, coins: 0, nextCooldownSec: 1800 };
203
233
  await humanDelay(500, 1000);
204
234
  }
205
235
 
206
236
  await sleep(2000);
207
237
  await channel.send('pls stream');
208
238
  response = await waitForDankMemer(12000);
209
- if (!response) return { result: 'no response after buy', coins: 0 };
239
+ if (!response) return { result: 'no response after buy', coins: 0, nextCooldownSec: 180 };
210
240
  await hydrate(response);
211
241
  logMsg(response, 'stream-retry');
212
242
  text = getFullText(response);
@@ -94,8 +94,26 @@ const RE_HOLD_TIGHT_REASON = /Reason:\s*\/(\w+)/i;
94
94
  // ── Sleep ────────────────────────────────────────────────────
95
95
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
96
96
 
97
- function humanDelay(min = 50, max = 200) {
98
- return new Promise(r => setTimeout(r, min + Math.random() * (max - min)));
97
+ // ── Advanced Human Delay (Anti-Detection) ────────────────────
98
+ // Natural human-like timing: 0.5-1.0s base with subtle variations
99
+ let _antiDetect = null;
100
+ try {
101
+ _antiDetect = require('../antiDetect');
102
+ } catch {}
103
+
104
+ function humanDelay(contextOrMin, max) {
105
+ // Backward compatibility: if called with numeric args, use simple delay
106
+ if (typeof contextOrMin === 'number') {
107
+ const min = contextOrMin;
108
+ const maxVal = typeof max === 'number' ? max : 1000;
109
+ return new Promise(r => setTimeout(r, min + Math.random() * (maxVal - min)));
110
+ }
111
+ // New API: context-based adaptive delay (uses 0.5-1s range)
112
+ if (_antiDetect && typeof contextOrMin === 'object') {
113
+ return _antiDetect.humanDelay(contextOrMin);
114
+ }
115
+ // Default: natural human timing (0.5-1.0s)
116
+ return new Promise(r => setTimeout(r, 500 + Math.random() * 500));
99
117
  }
100
118
 
101
119
  // ── Message Parsing ──────────────────────────────────────────
@@ -23,7 +23,7 @@ const {
23
23
 
24
24
  const RE_MEMORY_BACKTICK_CHUNK = /`([^`]+)`/g;
25
25
  const RE_BACKTICK_STRIP = /`/g;
26
- const RE_WORK_COOLDOWN_TS = /<t:(\d+):R>/;
26
+ const RE_WORK_COOLDOWN_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
27
27
  const RE_WORK_COOLDOWN_MINUTES = /(\d+)\s*minute/i;
28
28
  const RE_WORK_COOLDOWN_HOURS = /(\d+)\s*hour/i;
29
29
 
@@ -78,18 +78,42 @@ function parseMemoryOrder(text) {
78
78
  * Patterns: "next shift <t:TIMESTAMP:R>", "X minutes", "X hours"
79
79
  */
80
80
  function parseWorkCooldown(text) {
81
- // Unix timestamp pattern: <t:TIMESTAMP:R>
82
- const tsMatch = text.match(RE_WORK_COOLDOWN_TS);
83
- if (tsMatch) {
84
- const ts = parseInt(tsMatch[1]);
85
- const now = Math.floor(Date.now() / 1000);
86
- const diff = ts - now;
87
- return diff > 0 ? diff : 3600;
81
+ const clean = String(stripAnsi(text || ''));
82
+ const lower = normalizeLower(clean).replace(/\s+/g, ' ').trim();
83
+
84
+ // Unix timestamp pattern: <t:TIMESTAMP:*>
85
+ const now = Math.floor(Date.now() / 1000);
86
+ const tsMatches = Array.from(clean.matchAll(RE_WORK_COOLDOWN_TS));
87
+ if (tsMatches.length > 0) {
88
+ let best = null;
89
+ for (const m of tsMatches) {
90
+ const ts = parseInt(m[1], 10);
91
+ if (!Number.isFinite(ts)) continue;
92
+ const diff = ts - now;
93
+ if (diff <= 0) continue;
94
+ if (best == null || diff < best) best = diff;
95
+ }
96
+ if (best != null) return Math.max(5, best);
97
+ }
98
+
99
+ // Compact forms frequently seen in buttons/messages: "1h", "54m", "30s"
100
+ if (/(next shift|work again|cooldown|already done|shift)/i.test(lower)) {
101
+ const short = lower.match(/(\d+)\s*(h|hr|hrs|m|min|mins|s|sec|secs)\b/i);
102
+ if (short) {
103
+ const n = parseInt(short[1], 10);
104
+ const u = short[2].toLowerCase();
105
+ if (Number.isFinite(n) && n > 0) {
106
+ if (u.startsWith('h')) return n * 3600;
107
+ if (u.startsWith('m')) return n * 60;
108
+ return n;
109
+ }
110
+ }
88
111
  }
112
+
89
113
  // "X minutes" / "X hours" pattern
90
- const minMatch = text.match(RE_WORK_COOLDOWN_MINUTES);
114
+ const minMatch = clean.match(RE_WORK_COOLDOWN_MINUTES);
91
115
  if (minMatch) return parseInt(minMatch[1]) * 60;
92
- const hrMatch = text.match(RE_WORK_COOLDOWN_HOURS);
116
+ const hrMatch = clean.match(RE_WORK_COOLDOWN_HOURS);
93
117
  if (hrMatch) return parseInt(hrMatch[1]) * 3600;
94
118
  return null;
95
119
  }
@@ -311,7 +335,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
311
335
 
312
336
  if (!current) {
313
337
  LOG.warn('[work] No response');
314
- return { result: 'no response', coins: 0 };
338
+ return { result: 'no response', coins: 0, nextCooldownSec: 120 };
315
339
  }
316
340
 
317
341
  if (isHoldTight(current)) {
@@ -351,7 +375,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
351
375
 
352
376
  await channel.send('pls work shift');
353
377
  current = await waitForDankMemer(10000);
354
- if (!current) return { result: 'no response after apply', coins: 0 };
378
+ if (!current) return { result: 'no response after apply', coins: 0, nextCooldownSec: 180 };
355
379
  if (isCV2(current)) await ensureCV2(current);
356
380
  logMsg(current, 'work-after-apply');
357
381
  text = getFullText(current);
@@ -425,7 +449,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
425
449
  return { result: 'work shift completed', coins: 0, nextCooldownSec: finalCd || 3600 };
426
450
  }
427
451
 
428
- return { result: `work done`, coins: 0, nextCooldownSec: finalCd || null };
452
+ return { result: `work done`, coins: 0, nextCooldownSec: finalCd || 1800 };
429
453
  }
430
454
 
431
455
  module.exports = { runWorkShift, autoApplyForJob, resignFromJob, JOBS };
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Advanced Cooldown Manager with Smart CD Calculations
3
+ *
4
+ * Features:
5
+ * - EMA-based cooldown prediction (learns from history)
6
+ * - Command chaining with optimal timing
7
+ * - Priority queue for scheduling
8
+ * - Redis-backed shared state (cluster mode)
9
+ * - Predictive readiness estimation
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const { LRUCache, MinHeap } = require('./structures');
15
+ const { calcSmartCooldown } = require('./antiDetect');
16
+
17
+ // ── Command Cooldown Config ───────────────────────────────────
18
+ const COMMAND_CONFIG = Object.freeze({
19
+ // Base cooldowns in seconds
20
+ beg: { base: 120, priority: 3, category: 'income' },
21
+ crime: { base: 1200, priority: 2, category: 'income' },
22
+ search: { base: 150, priority: 3, category: 'income' },
23
+ work: { base: 3600, priority: 1, category: 'income' },
24
+ dig: { base: 600, priority: 2, category: 'income' },
25
+ fish: { base: 900, priority: 2, category: 'income' },
26
+ hunt: { base: 120, priority: 3, category: 'income' },
27
+ farm: { base: 300, priority: 2, category: 'income' },
28
+ stream: { base: 120, priority: 3, category: 'income' },
29
+ trivia: { base: 180, priority: 2, category: 'income' },
30
+ highlow: { base: 120, priority: 2, category: 'income' },
31
+ gamble: { base: 30, priority: 1, category: 'gambling' },
32
+ blackjack: { base: 30, priority: 1, category: 'gambling' },
33
+ roulette: { base: 30, priority: 1, category: 'gambling' },
34
+ slots: { base: 30, priority: 1, category: 'gambling' },
35
+ adventure: { base: 1800, priority: 1, category: 'special' },
36
+ postmeme: { base: 300, priority: 2, category: 'social' },
37
+ meme: { base: 300, priority: 2, category: 'social' },
38
+ vote: { base: 7200, priority: 1, category: 'special' },
39
+ daily: { base: 86400, priority: 1, category: 'special' },
40
+ weekly: { base: 604800, priority: 1, category: 'special' },
41
+ monthly: { base: 2592000, priority: 1, category: 'special' },
42
+ });
43
+
44
+ // ── EMA Calculator ────────────────────────────────────────────
45
+ /**
46
+ * Exponential Moving Average for cooldown prediction.
47
+ * Gives more weight to recent observations.
48
+ */
49
+ class EMACalculator {
50
+ constructor(alpha = 0.3) {
51
+ this.alpha = alpha; // Smoothing factor (0-1)
52
+ this.ema = null;
53
+ }
54
+
55
+ update(value) {
56
+ if (this.ema === null) {
57
+ this.ema = value;
58
+ } else {
59
+ this.ema = this.alpha * value + (1 - this.alpha) * this.ema;
60
+ }
61
+ return this.ema;
62
+ }
63
+
64
+ getPrediction() {
65
+ return this.ema;
66
+ }
67
+
68
+ reset() {
69
+ this.ema = null;
70
+ }
71
+ }
72
+
73
+ // ── Cooldown Tracker ──────────────────────────────────────────
74
+ class CooldownTracker {
75
+ constructor(userId, redis = null, clusterEnabled = false) {
76
+ this.userId = userId;
77
+ this.redis = redis;
78
+ this.clusterEnabled = clusterEnabled;
79
+
80
+ // Local caches
81
+ this.cooldowns = new Map(); // command -> readyAt (timestamp)
82
+ this.emaTrackers = new Map(); // command -> EMACalculator
83
+ this.historyCache = new LRUCache(50); // Last 50 cooldown observations per command
84
+ }
85
+
86
+ /**
87
+ * Record a cooldown for a command.
88
+ * @param {string} command - Command name
89
+ * @param {number} cooldownSec - Observed cooldown in seconds
90
+ */
91
+ recordCooldown(command, cooldownSec) {
92
+ const now = Date.now();
93
+ const readyAt = now + (cooldownSec * 1000);
94
+
95
+ // Store in local map
96
+ this.cooldowns.set(command, readyAt);
97
+
98
+ // Update EMA prediction
99
+ if (!this.emaTrackers.has(command)) {
100
+ this.emaTrackers.set(command, new EMACalculator(0.3));
101
+ }
102
+ const ema = this.emaTrackers.get(command);
103
+ ema.update(cooldownSec);
104
+
105
+ // Store in Redis for cluster mode
106
+ if (this.redis && this.clusterEnabled) {
107
+ const key = `dkg:cd:${this.userId}:${command}`;
108
+ this.redis.setex(key, cooldownSec + 60, JSON.stringify({
109
+ readyAt,
110
+ ema: ema.getPrediction(),
111
+ recorded: now,
112
+ })).catch(() => {});
113
+ }
114
+
115
+ // Cache history
116
+ const history = this.historyCache.get(command) || [];
117
+ history.push({ cooldownSec, timestamp: now });
118
+ if (history.length > 50) history.shift();
119
+ this.historyCache.set(command, history);
120
+ }
121
+
122
+ /**
123
+ * Check if a command is ready.
124
+ * @param {string} command - Command name
125
+ * @returns {Object} - { ready: boolean, waitMs: number, predicted: number|null }
126
+ */
127
+ isReady(command) {
128
+ const readyAt = this.cooldowns.get(command);
129
+ if (!readyAt) {
130
+ return { ready: true, waitMs: 0, predicted: null };
131
+ }
132
+
133
+ const now = Date.now();
134
+ const waitMs = Math.max(0, readyAt - now);
135
+
136
+ // Add smart buffer
137
+ const config = COMMAND_CONFIG[command];
138
+ if (config) {
139
+ const buffered = calcSmartCooldown(waitMs / 1000, { addBuffer: true, addVariance: false });
140
+ return {
141
+ ready: waitMs <= 0,
142
+ waitMs: waitMs > 0 ? buffered * 1000 : 0,
143
+ predicted: this.emaTrackers.get(command)?.getPrediction() || null,
144
+ };
145
+ }
146
+
147
+ return {
148
+ ready: waitMs <= 0,
149
+ waitMs,
150
+ predicted: this.emaTrackers.get(command)?.getPrediction() || null,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Get predicted cooldown for a command.
156
+ * @param {string} command - Command name
157
+ * @returns {number|null} - Predicted cooldown in seconds
158
+ */
159
+ getPredictedCooldown(command) {
160
+ return this.emaTrackers.get(command)?.getPrediction() || null;
161
+ }
162
+
163
+ /**
164
+ * Get optimal command order based on cooldowns.
165
+ * @param {string[]} commands - List of commands to consider
166
+ * @returns {Array} - Sorted commands with timing info
167
+ */
168
+ getOptimalOrder(commands) {
169
+ const now = Date.now();
170
+ const order = commands.map(cmd => {
171
+ const config = COMMAND_CONFIG[cmd] || { priority: 5 };
172
+ const status = this.isReady(cmd);
173
+ return {
174
+ command: cmd,
175
+ ready: status.ready,
176
+ waitMs: status.waitMs,
177
+ priority: config.priority,
178
+ category: config.category,
179
+ predicted: status.predicted,
180
+ };
181
+ });
182
+
183
+ // Sort: ready commands first (by priority), then by wait time
184
+ order.sort((a, b) => {
185
+ if (a.ready && !b.ready) return -1;
186
+ if (!a.ready && b.ready) return 1;
187
+ if (a.ready && b.ready) return a.priority - b.priority;
188
+ return a.waitMs - b.waitMs;
189
+ });
190
+
191
+ return order;
192
+ }
193
+
194
+ /**
195
+ * Get all commands that will be ready within a time window.
196
+ * @param {number} windowMs - Time window in milliseconds
197
+ * @returns {Array} - Commands ready within window
198
+ */
199
+ getReadyInWindow(windowMs) {
200
+ const now = Date.now();
201
+ const ready = [];
202
+
203
+ for (const [command, readyAt] of this.cooldowns.entries()) {
204
+ const waitMs = readyAt - now;
205
+ if (waitMs <= windowMs) {
206
+ ready.push({
207
+ command,
208
+ waitMs: Math.max(0, waitMs),
209
+ readyAt,
210
+ });
211
+ }
212
+ }
213
+
214
+ ready.sort((a, b) => a.waitMs - b.waitMs);
215
+ return ready;
216
+ }
217
+
218
+ /**
219
+ * Clear cooldown for a command.
220
+ * @param {string} command - Command name
221
+ */
222
+ clearCooldown(command) {
223
+ this.cooldowns.delete(command);
224
+ }
225
+
226
+ /**
227
+ * Clear all cooldowns.
228
+ */
229
+ clearAll() {
230
+ this.cooldowns.clear();
231
+ }
232
+ }
233
+
234
+ // ── Command Scheduler (Priority Queue) ────────────────────────
235
+ class CommandScheduler {
236
+ constructor(tracker) {
237
+ this.tracker = tracker;
238
+ this.queue = new MinHeap((a, b) => a.executeAt - b.executeAt);
239
+ this.pending = new Map(); // command -> node reference
240
+ }
241
+
242
+ /**
243
+ * Schedule a command for execution.
244
+ * @param {string} command - Command name
245
+ * @param {Function} executeFn - Function to execute
246
+ * @param {number} executeAt - Timestamp to execute at
247
+ */
248
+ schedule(command, executeFn, executeAt) {
249
+ // Remove existing scheduled command if any
250
+ this.unschedule(command);
251
+
252
+ const node = {
253
+ command,
254
+ executeFn,
255
+ executeAt,
256
+ added: Date.now(),
257
+ };
258
+
259
+ this.queue.push(node);
260
+ this.pending.set(command, node);
261
+
262
+ return node;
263
+ }
264
+
265
+ /**
266
+ * Cancel a scheduled command.
267
+ * @param {string} command - Command name
268
+ */
269
+ unschedule(command) {
270
+ const node = this.pending.get(command);
271
+ if (node) {
272
+ this.queue.remove(node);
273
+ this.pending.delete(command);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Get next command to execute.
279
+ * @returns {Object|null} - Next command node or null
280
+ */
281
+ peekNext() {
282
+ return this.queue.peek();
283
+ }
284
+
285
+ /**
286
+ * Execute next command if it's time.
287
+ * @returns {Object|null} - Executed command node or null
288
+ */
289
+ async executeNext() {
290
+ const node = this.queue.peek();
291
+ if (!node) return null;
292
+
293
+ const now = Date.now();
294
+ if (node.executeAt <= now) {
295
+ this.queue.pop();
296
+ this.pending.delete(node.command);
297
+
298
+ try {
299
+ await node.executeFn();
300
+ return node;
301
+ } catch (e) {
302
+ console.error(`[scheduler] Error executing ${node.command}:`, e);
303
+ throw e;
304
+ }
305
+ }
306
+
307
+ return null;
308
+ }
309
+
310
+ /**
311
+ * Get queue size.
312
+ */
313
+ size() {
314
+ return this.queue.size();
315
+ }
316
+ }
317
+
318
+ // ── Factory Function ──────────────────────────────────────────
319
+ /**
320
+ * Create a cooldown manager for a user.
321
+ * @param {string} userId - User ID
322
+ * @param {Object} options
323
+ * @param {Object} options.redis - Redis client (optional)
324
+ * @param {boolean} options.clusterEnabled - Cluster mode flag
325
+ * @returns {Object} - { tracker, scheduler, COMMAND_CONFIG }
326
+ */
327
+ function createCooldownManager(userId, options = {}) {
328
+ const { redis = null, clusterEnabled = false } = options;
329
+
330
+ const tracker = new CooldownTracker(userId, redis, clusterEnabled);
331
+ const scheduler = new CommandScheduler(tracker);
332
+
333
+ return {
334
+ tracker,
335
+ scheduler,
336
+ COMMAND_CONFIG,
337
+ };
338
+ }
339
+
340
+ // ── Exports ───────────────────────────────────────────────────
341
+ module.exports = {
342
+ EMACalculator,
343
+ CooldownTracker,
344
+ CommandScheduler,
345
+ createCooldownManager,
346
+ COMMAND_CONFIG,
347
+ };
package/lib/grinder.js CHANGED
@@ -316,37 +316,48 @@ function renderDashboard() {
316
316
  const tw = Math.min(process.stdout.columns || 80, 78);
317
317
  const thinBar = c.dim + '─'.repeat(tw) + c.reset;
318
318
  const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
319
+ const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
320
+ const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
319
321
 
320
322
  // Header with dynamic version, command count, and status
321
- lines.push(bar);
323
+ lines.push(doubleBar);
322
324
  const cmdCount = AccountWorker.COMMAND_MAP.length;
323
325
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
324
326
  const mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
327
+
328
+ // Animated spinner based on time
329
+ const spinners = ['◐', '◓', '◑', '◒'];
330
+ const spinner = spinners[Math.floor(Date.now() / 250) % 4];
331
+ const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
332
+
325
333
  lines.push(
326
334
  ` ${rgb(139, 92, 246)}${c.bold}DankGrinder${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}` +
327
335
  ` ${c.dim}·${c.reset} ${c.white}${cmdCount} Cmds${c.reset}` +
328
336
  ` ${c.dim}·${c.reset} ${mode}` +
329
- ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
337
+ ` ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
330
338
  );
331
339
 
332
- // Stats row
333
- const liveIcon = rgb(52, 211, 153) + '' + c.reset;
340
+ // Stats row with enhanced visual indicators
341
+ const liveIcon = rgb(52, 211, 153) + '' + c.reset;
334
342
  const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
335
- const earnStr = `${rgb(52, 211, 153)} ${formatCoins(totalCoins)}${c.reset}`;
343
+ const earnStr = `${rgb(52, 211, 153)} ${formatCoins(totalCoins)}${c.reset}`;
336
344
  // Coins/hour rate
337
345
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
338
346
  const coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
339
347
  const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
340
348
  const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
341
349
  const rateStr = successRate >= 95
342
- ? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
343
- : successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
344
- : `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
345
- const upStr = `${rgb(251, 191, 36)} ${formatUptime()}${c.reset}`;
346
- // Memory usage (RSS in MB)
350
+ ? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
351
+ : successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
352
+ : `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
353
+ const upStr = `${rgb(251, 191, 36)} ${formatUptime()}${c.reset}`;
354
+ // Memory usage (RSS in MB) with bar indicator
347
355
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
356
+ const memPct = Math.min(100, (memMB / 1024) * 100);
357
+ const memBarWidth = Math.floor((memPct / 100) * 10);
358
+ const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
348
359
  const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
349
- const memStr = `${memColor}${memMB}MB${c.reset}`;
360
+ const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
350
361
  // Commands/minute from SlidingWindowCounter
351
362
  const cpmVal = globalCmdRate.getRate().toFixed(1);
352
363
  const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
@@ -394,9 +405,16 @@ function renderDashboard() {
394
405
  const bal = wk.stats.balance > 0
395
406
  ? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
396
407
  : `${c.dim}⏣ -${c.reset}`;
397
- const earned = wk.stats.coins > 0
398
- ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins).padStart(6)}${c.reset}`
399
- : `${c.dim} +0${c.reset}`;
408
+
409
+ // Mini progress bar for earned coins (visual indicator of activity)
410
+ const earnedNum = wk.stats.coins || 0;
411
+ const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
412
+ const earnedBar = earnedNum > 0
413
+ ? `${rgb(52, 211, 153)}${'▰'.repeat(earnedBarWidth)}${c.dim}${'▱'.repeat(5 - earnedBarWidth)}${c.reset}`
414
+ : `${c.dim}▱▱▱▱▱${c.reset}`;
415
+ const earned = earnedNum > 0
416
+ ? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
417
+ : `${c.dim}+0${c.reset} ${earnedBar}`;
400
418
 
401
419
  lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
402
420
  };
@@ -451,7 +469,7 @@ function renderDashboard() {
451
469
  }
452
470
  }
453
471
 
454
- lines.push(bar);
472
+ lines.push(doubleBarBot);
455
473
 
456
474
  // Absolute cursor home — always draw from row 1
457
475
  process.stdout.write('\x1b[H');
@@ -764,6 +782,13 @@ class MinHeap {
764
782
  // ══════════════════════════════════════════════════════════════
765
783
 
766
784
  class AccountWorker {
785
+ static SMART_CD_FLOORS = Object.freeze({
786
+ 'farm': 30,
787
+ 'adventure': 300,
788
+ 'stream': 600,
789
+ 'work shift': 1800,
790
+ });
791
+
767
792
  constructor(account, idx) {
768
793
  this.account = account;
769
794
  this.idx = idx;
@@ -1147,7 +1172,7 @@ class AccountWorker {
1147
1172
  lastErr = e;
1148
1173
  if (attempt < tries) {
1149
1174
  this.log('warn', `Inventory attempt ${attempt}/${tries} failed (${e.message}). Retrying...`);
1150
- await sleep(1500 + Math.floor(Math.random() * 1500));
1175
+ await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
1151
1176
  continue;
1152
1177
  }
1153
1178
  }
@@ -1667,7 +1692,7 @@ class AccountWorker {
1667
1692
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
1668
1693
  // Fast grinders — 10s CD
1669
1694
  { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
1670
- { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 6 },
1695
+ { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
1671
1696
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
1672
1697
  { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
1673
1698
  // Medium grinders — 20-25s CD
@@ -1681,10 +1706,10 @@ class AccountWorker {
1681
1706
  { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
1682
1707
  { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
1683
1708
  // Interactive — response-driven CD (handler sets nextCooldownSec)
1684
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 10, priority: 3 },
1685
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 10, priority: 3 },
1709
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
1710
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
1686
1711
  { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 10, priority: 3 },
1687
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 10, priority: 3 },
1712
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
1688
1713
  // Time-gated (run ASAP when available)
1689
1714
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
1690
1715
  { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
@@ -2029,8 +2054,6 @@ class AccountWorker {
2029
2054
  const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
2030
2055
  const totalWait = cd + jitterBase + microPause;
2031
2056
 
2032
- await this.setCooldown(item.cmd, totalWait);
2033
-
2034
2057
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
2035
2058
  const gapBase = cd <= 5 ? 1500 : cd <= 20 ? 2000 : 2500;
2036
2059
  const jitterGap = patternMod.minDelay + Math.random() * (patternMod.maxDelay - patternMod.minDelay);
@@ -2059,7 +2082,7 @@ class AccountWorker {
2059
2082
  // Grace period for interactive (button-click) commands — Dank Memer
2060
2083
  // needs time to process the interaction before accepting the next command.
2061
2084
  // Without this, the next command gets "Hold Tight" errors.
2062
- const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish']);
2085
+ const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
2063
2086
  if (INTERACTIVE_CMDS.has(item.cmd)) {
2064
2087
  await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
2065
2088
  }
@@ -2078,8 +2101,7 @@ class AccountWorker {
2078
2101
  this.failStreak = 0;
2079
2102
  }
2080
2103
 
2081
- this.lastCommandRun = Date.now();
2082
- await this.setCooldown(item.cmd, totalWait);
2104
+ this.lastCommandRun = Date.now();
2083
2105
 
2084
2106
  // Exponential backoff: if too many consecutive failures, slow down
2085
2107
  const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
@@ -2087,12 +2109,25 @@ class AccountWorker {
2087
2109
  const MIN_FAIL_COOLDOWN = 5;
2088
2110
 
2089
2111
  if (this.commandQueue && this.running && !shutdownCalled) {
2090
- let effectiveWait = this._lastCooldownOverride || totalWait;
2112
+ const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
2113
+ let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
2091
2114
  this._lastCooldownOverride = null;
2115
+
2116
+ // Smart fallback floors for long/interactive commands when parser misses exact cooldown.
2117
+ if (!hasOverride) {
2118
+ const floor = AccountWorker.SMART_CD_FLOORS[item.cmd];
2119
+ if (Number.isFinite(floor) && floor > 0) {
2120
+ effectiveWait = Math.max(effectiveWait, floor);
2121
+ }
2122
+ }
2123
+
2092
2124
  if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
2093
2125
  effectiveWait = MIN_FAIL_COOLDOWN;
2094
2126
  }
2095
- item.nextRunAt = Date.now() + effectiveWait * 1000 * backoffMultiplier;
2127
+
2128
+ const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
2129
+ await this.setCooldown(item.cmd, scheduledWaitSec);
2130
+ item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
2096
2131
  this.commandQueue.push(item);
2097
2132
  }
2098
2133
 
@@ -2438,17 +2473,31 @@ async function start(apiKey, apiUrl) {
2438
2473
  console.log('');
2439
2474
 
2440
2475
  // Phase 1: Login all accounts (staggered to avoid 429s)
2441
- const BATCH_SIZE = 5;
2442
- const BATCH_DELAY_MS = 3000;
2476
+ const LOGIN_PROGRESS_EVERY = 5;
2477
+ const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '100'), 10);
2478
+ const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '300'), 10);
2479
+ const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 100;
2480
+ const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 300);
2481
+
2482
+ const randomLoginGap = () => {
2483
+ if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
2484
+ return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2485
+ };
2486
+
2443
2487
  for (let i = 0; i < accounts.length; i++) {
2444
2488
  if (shutdownCalled) break;
2445
2489
  const worker = new AccountWorker(accounts[i], i);
2446
2490
  workers.push(worker);
2447
2491
  workerMap.set(accounts[i].id, worker);
2448
2492
  await worker.start();
2449
- if ((i + 1) % BATCH_SIZE === 0 && i + 1 < accounts.length) {
2450
- log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next batch in ${BATCH_DELAY_MS / 1000}s...${c.reset}`);
2451
- await new Promise(r => setTimeout(r, BATCH_DELAY_MS));
2493
+ if (i + 1 < accounts.length) {
2494
+ const gapMs = randomLoginGap();
2495
+ if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2496
+ log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next account in ${gapMs}ms...${c.reset}`);
2497
+ }
2498
+ await new Promise(r => setTimeout(r, gapMs));
2499
+ }
2500
+ if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2452
2501
  hintGC();
2453
2502
  }
2454
2503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.23.0",
3
+ "version": "5.25.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"