dankgrinder 5.260.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
@@ -1184,10 +1184,9 @@ class AccountWorker {
1184
1184
  accountId: this.account.id,
1185
1185
  redis,
1186
1186
  onPageProgress: ({ page, total }) => {
1187
- // Update same line instead of spamming new lines - uses cursor control
1188
- const progress = `[inv] ${this.username}: ${page}/${total} pages`;
1189
- const erase = '\x1b[2K\r'; // Clear line + carriage return
1190
- process.stdout.write(`${erase}${this.color}${progress}${c.reset}`);
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}`);
1191
1190
  },
1192
1191
  });
1193
1192
 
@@ -2561,25 +2560,25 @@ async function start(apiKey, apiUrl) {
2561
2560
  hintGC();
2562
2561
  }
2563
2562
 
2564
- // Phase 2: Run inventory on ALL accounts (must complete before any grinding)
2565
- 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
2566
2567
  let invDone = 0;
2567
2568
  let invFailed = 0;
2569
+ const total = workers.length;
2568
2570
 
2569
- // Use sequential inventory checks with single-line progress update
2570
- const totalAccounts = workers.length;
2571
- for (let i = 0; i < workers.length; i++) {
2572
- const w = workers[i];
2573
- const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2571
+ await Promise.all(workers.map(async (w, i) => {
2572
+ const label = w?.username || w?.account?.label || 'account';
2574
2573
 
2575
- // Update single line instead of spamming new lines
2576
- const progressLine = `\x1b[2K\r${c.dim}[inv] ${i + 1}/${totalAccounts}: ${label}${c.reset}`;
2577
- process.stdout.write(progressLine);
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}`);
2578
2577
 
2579
2578
  try {
2580
2579
  const invRes = await w.checkInventory({
2581
2580
  force: true,
2582
- startupProgress: { current: i + 1, total: workers.length },
2581
+ startupProgress: { current: i + 1, total },
2583
2582
  requireComplete: true,
2584
2583
  maxAttempts: 3,
2585
2584
  });
@@ -2588,11 +2587,11 @@ async function start(apiKey, apiUrl) {
2588
2587
  } catch {
2589
2588
  invFailed++;
2590
2589
  }
2591
- }
2590
+ }));
2592
2591
 
2593
- // Newline after progress
2592
+ // Final newline and summary
2594
2593
  process.stdout.write('\n');
2595
- log('success', `${c.dim}Inventory: ${invDone}/${workers.length} complete${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}${c.reset}`);
2594
+ log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
2596
2595
 
2597
2596
  if (invFailed > 0) {
2598
2597
  log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
@@ -2610,6 +2609,8 @@ async function start(apiKey, apiUrl) {
2610
2609
  startTime = Date.now();
2611
2610
  dashboardStarted = true;
2612
2611
  setDashboardActive(true);
2612
+ // Setup keyboard shortcuts
2613
+ setupKeyboardShortcuts();
2613
2614
  // Clear entire screen so startup logs don't create ghost bars
2614
2615
  process.stdout.write('\x1b[2J\x1b[H');
2615
2616
  process.stdout.write(c.hide);
@@ -2753,4 +2754,76 @@ async function start(apiKey, apiUrl) {
2753
2754
  });
2754
2755
  }
2755
2756
 
2756
- 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.260.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",