dankgrinder 5.25.0 → 5.281.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/afkMode.js ADDED
@@ -0,0 +1,349 @@
1
+ /**
2
+ * AFK Mode - Smart Command Queuing
3
+ *
4
+ * Users want to run the bot and forget about it. This module provides:
5
+ * - Smart prioritization based on cooldowns and earnings
6
+ * - Auto-deposit when wallet is full
7
+ * - Auto-sell junk items
8
+ * - Sleep mode (pause during night hours)
9
+ * - Smart bank management
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ // ── Configuration ─────────────────────────────────────────────
15
+ const AFK_CONFIG = Object.freeze({
16
+ // Wallet management
17
+ AUTO_DEPOSIT_THRESHOLD: 100000, // Deposit when wallet > 100k
18
+ AUTO_DEPOSIT_KEEP: 20000, // Keep 20k in wallet after deposit
19
+
20
+ // Inventory management
21
+ AUTO_SELL_THRESHOLD: 0.85, // Sell when inventory > 85% full
22
+ JUNK_ITEMS: new Set([ // Always sell these
23
+ 'trash', 'junk', 'rock', 'stick', 'bottle', 'can', 'old shoe',
24
+ 'broken compass', 'rusty key', 'tattered flag', 'empty chest',
25
+ ]),
26
+ VALUABLE_ITEMS: new Set([ // Never sell these
27
+ 'diamond', 'ruby', 'emerald', 'sapphire', 'gold bar', 'coin',
28
+ 'legendary', 'mythical', 'artifact', 'treasure',
29
+ ]),
30
+
31
+ // Sleep mode (optional - pause during these hours)
32
+ SLEEP_MODE_ENABLED: false,
33
+ SLEEP_START_HOUR: 2, // 2 AM
34
+ SLEEP_END_HOUR: 6, // 6 AM
35
+
36
+ // Command prioritization
37
+ HIGH_VALUE_COMMANDS: new Set(['daily', 'weekly', 'monthly', 'work', 'stream']),
38
+ INCOME_COMMANDS: new Set(['beg', 'search', 'crime', 'hunt', 'dig', 'fish']),
39
+ GAMBLE_COMMANDS: new Set(['blackjack', 'roulette', 'slots', 'snakeeyes', 'cointoss']),
40
+
41
+ // Gambling limits in AFK mode
42
+ GAMBLE_MAX_LOSSES: 10, // Stop after 10 losses
43
+ GAMBLE_MAX_LOSS_AMOUNT: 50000, // Stop after losing 50k
44
+ });
45
+
46
+ // ── Command Priority Calculator ───────────────────────────────
47
+ /**
48
+ * Calculate priority score for a command.
49
+ * Higher score = should run sooner.
50
+ */
51
+ function calculateCommandPriority(cmdName, cooldownMs, account) {
52
+ let score = 0;
53
+
54
+ // Ready commands get highest priority
55
+ if (cooldownMs <= 0) {
56
+ score += 1000;
57
+ } else {
58
+ // Penalize by wait time (in minutes)
59
+ score -= Math.floor(cooldownMs / 60000);
60
+ }
61
+
62
+ // High-value commands get bonus
63
+ if (AFK_CONFIG.HIGH_VALUE_COMMANDS.has(cmdName)) {
64
+ score += 500;
65
+ }
66
+
67
+ // Income commands are important
68
+ if (AFK_CONFIG.INCOME_COMMANDS.has(cmdName)) {
69
+ score += 200;
70
+ }
71
+
72
+ // Gambling commands are lower priority in AFK mode
73
+ if (AFK_CONFIG.GAMBLE_COMMANDS.has(cmdName)) {
74
+ score -= 100;
75
+ }
76
+
77
+ // Periodic commands (daily/weekly/monthly) get huge bonus when ready
78
+ const now = Date.now();
79
+ const dailyCd = account?.doneToday?.get('daily') || 0;
80
+ const weeklyCd = account?.doneToday?.get('weekly') || 0;
81
+ const monthlyCd = account?.doneToday?.get('monthly') || 0;
82
+
83
+ if (cmdName === 'daily' && dailyCd <= now) score += 800;
84
+ if (cmdName === 'weekly' && weeklyCd <= now) score += 900;
85
+ if (cmdName === 'monthly' && monthlyCd <= now) score += 1000;
86
+
87
+ return score;
88
+ }
89
+
90
+ // ── Smart Command Queue Builder ───────────────────────────────
91
+ /**
92
+ * Build optimized command queue based on cooldowns and priorities.
93
+ */
94
+ function buildSmartQueue(account, cooldowns) {
95
+ const enabledCmds = getEnabledCommands(account);
96
+ const now = Date.now();
97
+
98
+ // Calculate priority for each command
99
+ const priorities = enabledCmds.map(cmd => {
100
+ const cdUntil = cooldowns.get(cmd) || 0;
101
+ const cooldownMs = Math.max(0, cdUntil - now);
102
+ const priority = calculateCommandPriority(cmd, cooldownMs, account);
103
+
104
+ return { cmd, priority, cooldownMs };
105
+ });
106
+
107
+ // Sort by priority (highest first)
108
+ priorities.sort((a, b) => b.priority - a.priority);
109
+
110
+ // Return queue of command names
111
+ return priorities.map(p => p.cmd);
112
+ }
113
+
114
+ // ── Get Enabled Commands ──────────────────────────────────────
115
+ function getEnabledCommands(account) {
116
+ const cmdMap = [
117
+ { key: 'cmd_beg', name: 'beg' },
118
+ { key: 'cmd_search', name: 'search' },
119
+ { key: 'cmd_crime', name: 'crime' },
120
+ { key: 'cmd_hunt', name: 'hunt' },
121
+ { key: 'cmd_dig', name: 'dig' },
122
+ { key: 'cmd_fish', name: 'fish' },
123
+ { key: 'cmd_farm', name: 'farm' },
124
+ { key: 'cmd_work', name: 'work' },
125
+ { key: 'cmd_stream', name: 'stream' },
126
+ { key: 'cmd_daily', name: 'daily' },
127
+ { key: 'cmd_weekly', name: 'weekly' },
128
+ { key: 'cmd_monthly', name: 'monthly' },
129
+ { key: 'cmd_highlow', name: 'hl' },
130
+ { key: 'cmd_blackjack', name: 'blackjack' },
131
+ { key: 'cmd_roulette', name: 'roulette' },
132
+ { key: 'cmd_slots', name: 'slots' },
133
+ { key: 'cmd_snakeeyes', name: 'snakeeyes' },
134
+ { key: 'cmd_trivia', name: 'trivia' },
135
+ { key: 'cmd_scratch', name: 'scratch' },
136
+ { key: 'cmd_adventure', name: 'adventure' },
137
+ { key: 'cmd_postmemes', name: 'postmemes' },
138
+ { key: 'cmd_tidy', name: 'tidy' },
139
+ { key: 'cmd_use', name: 'use' },
140
+ { key: 'cmd_deposit', name: 'deposit' },
141
+ ];
142
+
143
+ return cmdMap
144
+ .filter(c => account[c.key] === true)
145
+ .map(c => c.name);
146
+ }
147
+
148
+ // ── Auto-Deposit Check ────────────────────────────────────────
149
+ /**
150
+ * Check if auto-deposit should run.
151
+ */
152
+ function shouldAutoDeposit(walletBalance, account) {
153
+ if (!account.cmd_deposit) return false;
154
+
155
+ const threshold = account.afk_deposit_threshold || AFK_CONFIG.AUTO_DEPOSIT_THRESHOLD;
156
+ return walletBalance > threshold;
157
+ }
158
+
159
+ // ── Auto-Sell Check ───────────────────────────────────────────
160
+ /**
161
+ * Check if inventory auto-sell should run.
162
+ */
163
+ function shouldAutoSell(inventoryCount, maxInventory, account) {
164
+ if (!account.cmd_use) return false;
165
+
166
+ const threshold = account.afk_sell_threshold || AFK_CONFIG.AUTO_SELL_THRESHOLD;
167
+ return inventoryCount / maxInventory >= threshold;
168
+ }
169
+
170
+ // ── Get Items To Sell ─────────────────────────────────────────
171
+ /**
172
+ * Determine which items to auto-sell.
173
+ */
174
+ function getItemsToSell(inventory) {
175
+ const toSell = [];
176
+
177
+ for (const item of inventory) {
178
+ const itemName = (item.name || '').toLowerCase();
179
+
180
+ // Always sell junk
181
+ if (AFK_CONFIG.JUNK_ITEMS.some(junk => itemName.includes(junk))) {
182
+ toSell.push(item.name);
183
+ continue;
184
+ }
185
+
186
+ // Never sell valuables
187
+ if (AFK_CONFIG.VALUABLE_ITEMS.some(val => itemName.includes(val))) {
188
+ continue;
189
+ }
190
+
191
+ // Sell common items (value < 1000)
192
+ if (item.value && item.value < 1000) {
193
+ toSell.push(item.name);
194
+ }
195
+ }
196
+
197
+ return toSell;
198
+ }
199
+
200
+ // ── Sleep Mode Check ──────────────────────────────────────────
201
+ /**
202
+ * Check if sleep mode is active.
203
+ */
204
+ function isSleepMode() {
205
+ if (!AFK_CONFIG.SLEEP_MODE_ENABLED) return false;
206
+
207
+ const hour = new Date().getHours();
208
+ const start = AFK_CONFIG.SLEEP_START_HOUR;
209
+ const end = AFK_CONFIG.SLEEP_END_HOUR;
210
+
211
+ if (start < end) {
212
+ return hour >= start && hour < end;
213
+ }
214
+ // Handles overnight ranges (e.g., 22:00 - 06:00)
215
+ return hour >= start || hour < end;
216
+ }
217
+
218
+ // ── AFK Mode Presets ──────────────────────────────────────────
219
+ const AFK_PRESETS = {
220
+ // Casual: Run every 2 hours, low priority commands only
221
+ casual: {
222
+ commands: ['beg', 'search', 'daily', 'weekly', 'monthly'],
223
+ deposit_threshold: 50000,
224
+ sell_threshold: 0.9,
225
+ gamble_enabled: false,
226
+ },
227
+
228
+ // Grinder: Run all commands ASAP, maximize earnings
229
+ grinder: {
230
+ commands: 'all',
231
+ deposit_threshold: 100000,
232
+ sell_threshold: 0.8,
233
+ gamble_enabled: true,
234
+ gamble_limit: 20,
235
+ },
236
+
237
+ // Smart: AI-driven prioritization based on cooldowns
238
+ smart: {
239
+ commands: 'all',
240
+ deposit_threshold: 100000,
241
+ sell_threshold: 0.85,
242
+ gamble_enabled: true,
243
+ gamble_limit: 10,
244
+ use_priority_queue: true,
245
+ },
246
+
247
+ // Sleep: Only run long cooldown commands
248
+ sleep: {
249
+ commands: ['daily', 'weekly', 'monthly', 'work'],
250
+ deposit_threshold: 200000,
251
+ sell_threshold: 0.95,
252
+ gamble_enabled: false,
253
+ },
254
+
255
+ // Safe: No gambling, conservative play
256
+ safe: {
257
+ commands: 'all',
258
+ deposit_threshold: 100000,
259
+ sell_threshold: 0.85,
260
+ gamble_enabled: false,
261
+ },
262
+ };
263
+
264
+ // ── Apply Preset ──────────────────────────────────────────────
265
+ /**
266
+ * Apply an AFK preset to an account.
267
+ */
268
+ function applyPreset(account, presetName) {
269
+ const preset = AFK_PRESETS[presetName];
270
+ if (!preset) return false;
271
+
272
+ // Set commands
273
+ if (preset.commands === 'all') {
274
+ // Enable all commands
275
+ account.cmd_beg = true;
276
+ account.cmd_search = true;
277
+ account.cmd_crime = true;
278
+ account.cmd_hunt = true;
279
+ account.cmd_dig = true;
280
+ account.cmd_fish = true;
281
+ account.cmd_farm = true;
282
+ account.cmd_work = true;
283
+ account.cmd_stream = true;
284
+ account.cmd_daily = true;
285
+ account.cmd_weekly = true;
286
+ account.cmd_monthly = true;
287
+ account.cmd_highlow = true;
288
+ account.cmd_blackjack = preset.gamble_enabled;
289
+ account.cmd_roulette = preset.gamble_enabled;
290
+ account.cmd_slots = preset.gamble_enabled;
291
+ account.cmd_snakeeyes = preset.gamble_enabled;
292
+ account.cmd_trivia = true;
293
+ account.cmd_scratch = true;
294
+ account.cmd_adventure = true;
295
+ account.cmd_postmemes = true;
296
+ account.cmd_tidy = true;
297
+ account.cmd_use = true;
298
+ account.cmd_deposit = true;
299
+ } else {
300
+ // Enable only specified commands
301
+ const cmdKeys = {
302
+ beg: 'cmd_beg', search: 'cmd_search', crime: 'cmd_crime',
303
+ hunt: 'cmd_hunt', dig: 'cmd_dig', fish: 'cmd_fish',
304
+ farm: 'cmd_farm', work: 'cmd_work', stream: 'cmd_stream',
305
+ daily: 'cmd_daily', weekly: 'cmd_weekly', monthly: 'cmd_monthly',
306
+ hl: 'cmd_highlow', blackjack: 'cmd_blackjack', roulette: 'cmd_roulette',
307
+ slots: 'cmd_slots', snakeeyes: 'cmd_snakeeyes', trivia: 'cmd_trivia',
308
+ scratch: 'cmd_scratch', adventure: 'cmd_adventure',
309
+ postmemes: 'cmd_postmemes', tidy: 'cmd_tidy',
310
+ use: 'cmd_use', deposit: 'cmd_deposit',
311
+ };
312
+
313
+ // Disable all first
314
+ Object.values(cmdKeys).forEach(key => { account[key] = false; });
315
+
316
+ // Enable selected
317
+ preset.commands.forEach(cmd => {
318
+ const key = cmdKeys[cmd];
319
+ if (key) account[key] = true;
320
+ });
321
+ }
322
+
323
+ // Set thresholds
324
+ account.afk_deposit_threshold = preset.deposit_threshold;
325
+ account.afk_sell_threshold = preset.sell_threshold;
326
+ account.afk_gamble_enabled = preset.gamble_enabled;
327
+ account.afk_gamble_limit = preset.gamble_limit || 10;
328
+
329
+ return true;
330
+ }
331
+
332
+ // ── Exports ───────────────────────────────────────────────────
333
+ module.exports = {
334
+ // Main functions
335
+ buildSmartQueue,
336
+ calculateCommandPriority,
337
+ shouldAutoDeposit,
338
+ shouldAutoSell,
339
+ getItemsToSell,
340
+ isSleepMode,
341
+ applyPreset,
342
+
343
+ // Getters
344
+ getEnabledCommands,
345
+
346
+ // Constants
347
+ AFK_CONFIG,
348
+ AFK_PRESETS,
349
+ };
package/lib/antiDetect.js CHANGED
@@ -34,6 +34,16 @@ const CONFIG = Object.freeze({
34
34
 
35
35
  // Pattern randomization
36
36
  PATTERN_RESET_CHANCE: 0.2, // 20% chance to reset timing pattern
37
+
38
+ // Session-based behavior (humans get tired/faster over time)
39
+ SESSION_TIRE_FACTOR: 0.001, // Get 0.1% slower per action
40
+ SESSION_MAX_TIRED: 0.15, // Max 15% slower when tired
41
+
42
+ // Time-of-day awareness (humans are slower at night)
43
+ TIME_DAWN: { hour: 6, multiplier: 0.9 }, // Faster in morning
44
+ TIME_DAY: { hour: 12, multiplier: 1.0 }, // Normal during day
45
+ TIME_EVENING: { hour: 18, multiplier: 1.05 }, // Slightly slower evening
46
+ TIME_NIGHT: { hour: 23, multiplier: 1.15 }, // Slower at night
37
47
  });
38
48
 
39
49
  // ── Session State ─────────────────────────────────────────────
@@ -42,7 +52,9 @@ const sessionState = {
42
52
  lastActionTime: 0,
43
53
  currentDrift: 0,
44
54
  patternSeed: Math.random(),
45
- activityLevel: 1.0, // Starts neutral
55
+ activityLevel: 1.0, // Starts neutral
56
+ tiredness: 0, // Increases with actions
57
+ sessionStart: Date.now(),
46
58
  };
47
59
 
48
60
  // ── Seeded Random for Reproducible Patterns ──────────────────
@@ -51,6 +63,30 @@ function seededRandom(seed) {
51
63
  return x - Math.floor(x);
52
64
  }
53
65
 
66
+ // ── Time-of-Day Multiplier ────────────────────────────────────
67
+ /**
68
+ * Get delay multiplier based on current hour.
69
+ * Humans are slower at night, faster in the morning.
70
+ */
71
+ function getTimeOfDayMultiplier() {
72
+ const hour = new Date().getHours();
73
+
74
+ // Simple interpolation between time periods
75
+ if (hour >= 6 && hour < 12) {
76
+ // Morning: faster
77
+ return 0.9 + (hour - 6) * 0.016; // 0.9 → 1.0
78
+ } else if (hour >= 12 && hour < 18) {
79
+ // Day: normal
80
+ return 1.0 + (hour - 12) * 0.008; // 1.0 → 1.05
81
+ } else if (hour >= 18 && hour < 23) {
82
+ // Evening: slightly slower
83
+ return 1.05 + (hour - 18) * 0.02; // 1.05 → 1.15
84
+ } else {
85
+ // Night: slowest
86
+ return 1.15;
87
+ }
88
+ }
89
+
54
90
  // ── Adaptive Delay Calculation ────────────────────────────────
55
91
  /**
56
92
  * Calculate human-like delay based on context.
@@ -101,6 +137,14 @@ async function calcAdaptiveDelay(options = {}) {
101
137
  );
102
138
  multiplier *= (1 + sessionState.currentDrift);
103
139
 
140
+ // Apply tiredness factor (humans get slower over long sessions)
141
+ sessionState.tiredness += CONFIG.SESSION_TIRE_FACTOR;
142
+ sessionState.tiredness = Math.min(CONFIG.SESSION_MAX_TIRED, sessionState.tiredness);
143
+ multiplier *= (1 + sessionState.tiredness);
144
+
145
+ // Apply time-of-day multiplier
146
+ multiplier *= getTimeOfDayMultiplier();
147
+
104
148
  // Pattern reset for unpredictability
105
149
  if (Math.random() < CONFIG.PATTERN_RESET_CHANCE) {
106
150
  sessionState.patternSeed = Math.random();
@@ -146,14 +190,20 @@ function resetSession() {
146
190
  sessionState.currentDrift = 0;
147
191
  sessionState.patternSeed = Math.random();
148
192
  sessionState.activityLevel = 1.0;
193
+ sessionState.tiredness = 0;
194
+ sessionState.sessionStart = Date.now();
149
195
  }
150
196
 
151
197
  // ── Get Session Stats (for debugging) ─────────────────────────
152
198
  function getSessionStats() {
199
+ const elapsed = Date.now() - sessionState.sessionStart;
153
200
  return {
154
201
  actionCount: sessionState.actionCount,
155
202
  currentDrift: sessionState.currentDrift,
203
+ tiredness: sessionState.tiredness,
156
204
  timeSinceLast: Date.now() - sessionState.lastActionTime,
205
+ sessionDuration: Math.round(elapsed / 60000), // minutes
206
+ timeMultiplier: getTimeOfDayMultiplier(),
157
207
  };
158
208
  }
159
209
 
package/lib/grinder.js CHANGED
@@ -605,6 +605,51 @@ function safeParseJSON(str, fallback = []) {
605
605
  try { return JSON.parse(str || '[]'); } catch { return fallback; }
606
606
  }
607
607
 
608
+ // ── Command Result Formatter ──────────────────────────────────
609
+ // Clean command names and format results with color codes
610
+ const CMD_NAMES_CLEAN = {
611
+ bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
612
+ ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
613
+ hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
614
+ tidy: 'Tidy', farm: 'Farm', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly',
615
+ scratch: 'Scratch', adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
616
+ drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
617
+ work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
618
+ };
619
+
620
+ function formatCommandName(cmd) {
621
+ if (!cmd) return '?';
622
+ const clean = cmd.replace(/^pls\s+/, '').replace(/\s+\d+.*$/, '').trim().toLowerCase();
623
+ return CMD_NAMES_CLEAN[clean] || clean.charAt(0).toUpperCase() + clean.slice(1);
624
+ }
625
+
626
+ function formatCommandResult(cmdName, result, earned, spent) {
627
+ const name = formatCommandName(cmdName);
628
+ const net = (earned || 0) - (spent || 0);
629
+
630
+ // Extract clean result text
631
+ let cleanResult = stripAnsi(result || '').replace(/\n/g, ' ').substring(0, 40);
632
+
633
+ // Check for common failure/hold patterns
634
+ if (cleanResult.toLowerCase().includes('hold tight')) return `${name}: ${c.yellow}Hold Tight${c.reset}`;
635
+ if (cleanResult.toLowerCase().includes('cooldown')) return `${name}: ${c.dim}On Cooldown${c.reset}`;
636
+ if (cleanResult.toLowerCase().includes('no response')) return `${name}: ${c.red}No Response${c.reset}`;
637
+
638
+ // Format with earnings/losses
639
+ if (net > 0) {
640
+ return `${name}: ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`;
641
+ } else if (net < 0) {
642
+ return `${name}: ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`;
643
+ } else if (earned === 0 && spent === 0) {
644
+ // No coins changed - show result context
645
+ if (cleanResult.includes('completed') || cleanResult.includes('done')) {
646
+ return `${name}: ${c.dim}Done${c.reset}`;
647
+ }
648
+ return `${name}: ${c.dim}${cleanResult || 'Complete'}${c.reset}`;
649
+ }
650
+ return `${name}: ${c.dim}${cleanResult || 'Complete'}${c.reset}`;
651
+ }
652
+
608
653
  // ── Coin Parser — prefers Net:/Winnings: fields, falls back to max ⏣ ──
609
654
  function parseCoins(text) {
610
655
  if (!text) return 0;
@@ -1139,7 +1184,9 @@ class AccountWorker {
1139
1184
  accountId: this.account.id,
1140
1185
  redis,
1141
1186
  onPageProgress: ({ page, total }) => {
1142
- this.log('info', `Inventory pages: ${page}/${total}`);
1187
+ // Minimal progress update on same line
1188
+ const erase = '\x1b[2K\r';
1189
+ process.stdout.write(`${erase}${this.color}[inv] ${page}/${total}${c.reset}`);
1143
1190
  },
1144
1191
  });
1145
1192
 
@@ -1147,6 +1194,8 @@ class AccountWorker {
1147
1194
  throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
1148
1195
  }
1149
1196
 
1197
+ // Add newline after inventory progress
1198
+ process.stdout.write('\n');
1150
1199
  this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1151
1200
  try {
1152
1201
  await fetch(`${API_URL}/api/grinder/inventory`, {
@@ -1549,7 +1598,8 @@ class AccountWorker {
1549
1598
 
1550
1599
  const earned = Math.max(0, cmdResult.coins || 0);
1551
1600
  const spent = Math.max(0, cmdResult.lost || 0);
1552
- if (earned > 0) this.stats.coins += earned;
1601
+ // Track net earnings (add wins, subtract losses)
1602
+ this.stats.coins += (earned - spent);
1553
1603
  if (cmdResult.nextCooldownSec) {
1554
1604
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1555
1605
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
@@ -1596,8 +1646,9 @@ class AccountWorker {
1596
1646
  }
1597
1647
 
1598
1648
  this.stats.successes++;
1599
- const shortResult = result.substring(0, 30).replace(/\n/g, ' ');
1600
- this.setStatus(`${cmdName} ${shortResult}`);
1649
+ // Format result with clean command name and colored earnings
1650
+ const formattedResult = formatCommandResult(cmdName, result, earned, spent);
1651
+ this.setStatus(formattedResult);
1601
1652
  await sendLog(this.username, cmdName, result, 'success');
1602
1653
  reportEarnings(this.account.id, this.username, earned, spent, cmdName);
1603
1654
 
@@ -2346,8 +2397,8 @@ class AccountWorker {
2346
2397
  } catch {}
2347
2398
  }
2348
2399
 
2349
- // Let Discord gateway settle
2350
- await new Promise(r => setTimeout(r, 2500));
2400
+ // Let Discord gateway settle (reduced for faster startup)
2401
+ await new Promise(r => setTimeout(r, 500));
2351
2402
  resolve();
2352
2403
  });
2353
2404
 
@@ -2472,47 +2523,62 @@ async function start(apiKey, apiUrl) {
2472
2523
  console.log(` ${checks.join(' ')}`);
2473
2524
  console.log('');
2474
2525
 
2475
- // Phase 1: Login all accounts (staggered to avoid 429s)
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);
2526
+ // Phase 1: Login all accounts (optimized for speed)
2527
+ const LOGIN_PROGRESS_EVERY = 10;
2528
+ // Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
2529
+ const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2530
+ const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2531
+ const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
2532
+ const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 150);
2481
2533
 
2482
2534
  const randomLoginGap = () => {
2483
2535
  if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
2484
2536
  return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2485
2537
  };
2486
2538
 
2487
- for (let i = 0; i < accounts.length; i++) {
2539
+ // Parallel login in batches of 10 to avoid rate limits while being fast
2540
+ const BATCH_SIZE = 10;
2541
+ for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2488
2542
  if (shutdownCalled) break;
2489
- const worker = new AccountWorker(accounts[i], i);
2490
- workers.push(worker);
2491
- workerMap.set(accounts[i].id, worker);
2492
- await worker.start();
2493
- if (i + 1 < accounts.length) {
2543
+ const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
2544
+
2545
+ // Login batch in parallel
2546
+ await Promise.all(batch.map(async (acc, idx) => {
2547
+ const worker = new AccountWorker(acc, i + idx);
2548
+ workers.push(worker);
2549
+ workerMap.set(acc.id, worker);
2550
+ await worker.start();
2551
+ }));
2552
+
2553
+ // Small gap between batches
2554
+ if (i + BATCH_SIZE < accounts.length) {
2494
2555
  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
- }
2556
+ log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
2498
2557
  await new Promise(r => setTimeout(r, gapMs));
2499
2558
  }
2500
- if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2501
- hintGC();
2502
- }
2559
+
2560
+ hintGC();
2503
2561
  }
2504
2562
 
2505
- // Phase 2: Run inventory on ALL accounts (must complete before any grinding)
2506
- log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
2563
+ // Phase 2: Run inventory on ALL accounts in parallel (must complete before grinding)
2564
+ log('info', `${c.dim}Checking inventory for ${workers.length} accounts...${c.reset}`);
2565
+
2566
+ // Parallel inventory checks with single-line progress
2507
2567
  let invDone = 0;
2508
2568
  let invFailed = 0;
2569
+ const total = workers.length;
2570
+
2509
2571
  await Promise.all(workers.map(async (w, i) => {
2510
- const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2511
- log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label}${c.reset}`);
2572
+ const label = w?.username || w?.account?.label || 'account';
2573
+
2574
+ // Update progress on same line
2575
+ const progress = `[inv] ${invDone + invFailed + 1}/${total}: ${label}`;
2576
+ process.stdout.write(`\x1b[2K\r${c.dim}${progress}${c.reset}`);
2577
+
2512
2578
  try {
2513
2579
  const invRes = await w.checkInventory({
2514
2580
  force: true,
2515
- startupProgress: { current: i + 1, total: workers.length },
2581
+ startupProgress: { current: i + 1, total },
2516
2582
  requireComplete: true,
2517
2583
  maxAttempts: 3,
2518
2584
  });
@@ -2521,10 +2587,12 @@ async function start(apiKey, apiUrl) {
2521
2587
  } catch {
2522
2588
  invFailed++;
2523
2589
  }
2524
- const invComplete = invDone + invFailed;
2525
- log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
2526
2590
  }));
2527
2591
 
2592
+ // Final newline and summary
2593
+ process.stdout.write('\n');
2594
+ log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
2595
+
2528
2596
  if (invFailed > 0) {
2529
2597
  log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
2530
2598
  return;
@@ -2541,6 +2609,8 @@ async function start(apiKey, apiUrl) {
2541
2609
  startTime = Date.now();
2542
2610
  dashboardStarted = true;
2543
2611
  setDashboardActive(true);
2612
+ // Setup keyboard shortcuts
2613
+ setupKeyboardShortcuts();
2544
2614
  // Clear entire screen so startup logs don't create ghost bars
2545
2615
  process.stdout.write('\x1b[2J\x1b[H');
2546
2616
  process.stdout.write(c.hide);
@@ -2684,4 +2754,76 @@ async function start(apiKey, apiUrl) {
2684
2754
  });
2685
2755
  }
2686
2756
 
2687
- module.exports = { start };
2757
+ // ══════════════════════════════════════════════════════════════
2758
+ // ═ Keyboard Shortcuts (Quality of Life)
2759
+ // ══════════════════════════════════════════════════════════════
2760
+ // Single-key shortcuts for common actions
2761
+ function setupKeyboardShortcuts() {
2762
+ if (process.stdin.isTTY) {
2763
+ process.stdin.setRawMode(true);
2764
+ process.stdin.resume();
2765
+ process.stdin.setEncoding('utf8');
2766
+
2767
+ console.log(`\n ${c.dim}Keyboard shortcuts: ${c.reset}p=pause/resume all ${c.dim}·${c.reset} r=resume all ${c.dim}·${c.reset} s=status ${c.dim}·${c.reset} q=quit ${c.dim}·${c.reset} 1-9=toggle account`);
2768
+
2769
+ process.stdin.on('data', (key) => {
2770
+ const k = key.toString().toLowerCase();
2771
+
2772
+ // Ctrl+C or q = quit
2773
+ if (k === '\u0003' || k === 'q') {
2774
+ console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
2775
+ process.emit('SIGINT');
2776
+ return;
2777
+ }
2778
+
2779
+ // p = pause all accounts
2780
+ if (k === 'p') {
2781
+ let paused = 0;
2782
+ workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; paused++; } });
2783
+ console.log(`\n ${c.yellow}Paused ${paused} accounts${c.reset}`);
2784
+ return;
2785
+ }
2786
+
2787
+ // r = resume all accounts
2788
+ if (k === 'r') {
2789
+ let resumed = 0;
2790
+ workers.forEach(w => { if (w.paused) { w.paused = false; resumed++; } });
2791
+ console.log(`\n ${c.green}Resumed ${resumed} accounts${c.reset}`);
2792
+ return;
2793
+ }
2794
+
2795
+ // s = show status summary
2796
+ if (k === 's') {
2797
+ console.log(`\n ${c.bold}Status Summary:${c.reset}`);
2798
+ const active = workers.filter(w => w.running && !w.paused).length;
2799
+ const paused = workers.filter(w => w.paused).length;
2800
+ const offline = workers.filter(w => !w.running).length;
2801
+ const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
2802
+ console.log(` ${c.green}● ${active} active${c.reset} ${c.yellow}⏸ ${paused} paused${c.reset} ${c.red}○ ${offline} offline${c.reset} ${c.yellow}↻ ${recovering} recovering${c.reset}`);
2803
+ console.log(` ${c.dim}Total earnings: ⏣ ${workers.reduce((s, w) => s + (w.stats.coins || 0), 0).toLocaleString()}${c.reset}`);
2804
+ return;
2805
+ }
2806
+
2807
+ // 1-9 = toggle specific account (for small account counts)
2808
+ const num = parseInt(k, 10);
2809
+ if (num >= 1 && num <= 9 && workers[num - 1]) {
2810
+ const w = workers[num - 1];
2811
+ w.paused = !w.paused;
2812
+ console.log(`\n ${w.color}${w.username}${c.reset} ${w.paused ? c.yellow + 'paused' : c.green + 'resumed'}${c.reset}`);
2813
+ return;
2814
+ }
2815
+
2816
+ // ? = show help
2817
+ if (k === '?' || k === 'h') {
2818
+ console.log(`\n ${c.bold}Keyboard Shortcuts:${c.reset}`);
2819
+ console.log(` ${c.white}p${c.reset} Pause all accounts`);
2820
+ console.log(` ${c.white}r${c.reset} Resume all accounts`);
2821
+ console.log(` ${c.white}s${c.reset} Show status summary`);
2822
+ console.log(` ${c.white}q${c.reset} Quit gracefully`);
2823
+ console.log(` ${c.white}1-9${c.reset} Toggle account N`);
2824
+ console.log(` ${c.white}?${c.reset} Show this help`);
2825
+ return;
2826
+ }
2827
+ });
2828
+ }
2829
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.25.0",
3
+ "version": "5.281.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"
@@ -19,6 +19,10 @@
19
19
  ],
20
20
  "author": "DankGrinder",
21
21
  "license": "MIT",
22
+ "scripts": {
23
+ "start": "node bin/dankgrinder.js",
24
+ "test": "node --test test/**/*.test.js"
25
+ },
22
26
  "dependencies": {
23
27
  "debug": "^4.4.0",
24
28
  "discord.js-selfbot-v13": "3.5.0",