dankgrinder 1.0.1 → 4.0.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.
Files changed (2) hide show
  1. package/lib/grinder.js +1130 -210
  2. package/package.json +3 -2
package/lib/grinder.js CHANGED
@@ -1,35 +1,175 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
+ const Redis = require('ioredis');
2
3
 
4
+ // ── Terminal Colors & ANSI ───────────────────────────────────
3
5
  const c = {
4
- reset: '\x1b[0m',
5
- green: '\x1b[32m',
6
- red: '\x1b[31m',
7
- yellow: '\x1b[33m',
8
- cyan: '\x1b[36m',
9
- magenta: '\x1b[35m',
10
- dim: '\x1b[2m',
11
- bold: '\x1b[1m',
12
- white: '\x1b[37m',
13
- blue: '\x1b[34m',
6
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
7
+ green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m',
8
+ magenta: '\x1b[35m', white: '\x1b[37m', blue: '\x1b[34m',
9
+ bgGreen: '\x1b[42m', bgRed: '\x1b[41m', bgYellow: '\x1b[43m', bgCyan: '\x1b[46m',
10
+ bgMagenta: '\x1b[45m', bgBlue: '\x1b[44m', bgWhite: '\x1b[47m',
11
+ // Cursor control
12
+ clearLine: '\x1b[2K',
13
+ cursorUp: (n) => `\x1b[${n}A`,
14
+ cursorTo: (col) => `\x1b[${col}G`,
15
+ hide: '\x1b[?25l',
16
+ show: '\x1b[?25h',
17
+ saveCursor: '\x1b7',
18
+ restoreCursor: '\x1b8',
14
19
  };
15
20
 
16
- const ACCOUNT_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
21
+ const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
22
+ const DANK_MEMER_ID = '270904126974590976';
23
+
24
+ // ── Safe options for search/crime ──────────────────────────
25
+ const SAFE_SEARCH_LOCATIONS = [
26
+ 'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
27
+ 'couch', 'pantry', 'fridge', 'kitchen', 'bathroom', 'attic',
28
+ 'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
29
+ 'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
30
+ ];
31
+
32
+ const SAFE_CRIME_OPTIONS = [
33
+ 'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
34
+ 'money laundering', 'tax fraud', 'insurance fraud', 'scam',
35
+ ];
17
36
 
18
37
  let API_KEY = '';
19
38
  let API_URL = '';
39
+ let REDIS_URL = process.env.REDIS_URL || 'redis://default:qXcezFjDHlCDtakRUZJmvsEzHoBgdLHi@shortline.proxy.rlwy.net:32007';
40
+ let redis = null;
20
41
  const workers = [];
21
42
 
43
+ function initRedis() {
44
+ if (!redis) {
45
+ try {
46
+ redis = new Redis(REDIS_URL, { maxRetriesPerRequest: null, lazyConnect: true });
47
+ redis.connect().catch(() => {});
48
+ } catch (e) {
49
+ console.error('Redis connection failed', e);
50
+ }
51
+ }
52
+ }
53
+
54
+ // ── Live Dashboard State ─────────────────────────────────────
55
+ let dashboardLines = 0;
56
+ let dashboardStarted = false;
57
+ let totalBalance = 0;
58
+ let totalCoins = 0;
59
+ let totalCommands = 0;
60
+ let startTime = Date.now();
61
+ const recentLogs = []; // Ring buffer of last N log entries
62
+ const MAX_LOGS = 6;
63
+
64
+ const BANNER = `
65
+ ${c.magenta}${c.bold} ██████╗ █████╗ ███╗ ██╗██╗ ██╗${c.reset}
66
+ ${c.magenta}${c.bold} ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝${c.reset}
67
+ ${c.magenta}${c.bold} ██║ ██║███████║██╔██╗ ██║█████╔╝ ${c.reset}
68
+ ${c.magenta}${c.bold} ██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ${c.reset}
69
+ ${c.magenta}${c.bold} ██████╔╝██║ ██║██║ ╚████║██║ ██╗${c.reset}
70
+ ${c.magenta}${c.bold} ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝${c.reset}
71
+ ${c.cyan}${c.bold} ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗${c.reset}
72
+ ${c.cyan}${c.bold} ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗${c.reset}
73
+ ${c.cyan}${c.bold} ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝${c.reset}
74
+ ${c.cyan}${c.bold} ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗${c.reset}
75
+ ${c.cyan}${c.bold} ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║${c.reset}
76
+ ${c.cyan}${c.bold} ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝${c.reset}
77
+ `;
78
+
79
+ function formatUptime() {
80
+ const s = Math.floor((Date.now() - startTime) / 1000);
81
+ const h = Math.floor(s / 3600);
82
+ const m = Math.floor((s % 3600) / 60);
83
+ const sec = s % 60;
84
+ return `${h}h ${m}m ${sec}s`;
85
+ }
86
+
87
+ function formatCoins(n) {
88
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
89
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
90
+ return n.toLocaleString();
91
+ }
92
+
93
+ function renderDashboard() {
94
+ if (!dashboardStarted || workers.length === 0) return;
95
+
96
+ // Compute totals
97
+ totalBalance = 0; totalCoins = 0; totalCommands = 0;
98
+ for (const w of workers) {
99
+ totalBalance += w.stats.balance || 0;
100
+ totalCoins += w.stats.coins || 0;
101
+ totalCommands += w.stats.commands || 0;
102
+ }
103
+
104
+ const lines = [];
105
+ const width = Math.min(process.stdout.columns || 80, 90);
106
+ const sep = c.dim + '─'.repeat(width) + c.reset;
107
+
108
+ // Header bar
109
+ lines.push(sep);
110
+ lines.push(
111
+ ` ${c.yellow}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}` +
112
+ ` ${c.dim}│${c.reset} ${c.green}+⏣ ${formatCoins(totalCoins)}${c.reset} earned` +
113
+ ` ${c.dim}│${c.reset} ${c.white}${totalCommands}${c.reset} cmds` +
114
+ ` ${c.dim}│${c.reset} ${c.dim}⏱ ${formatUptime()}${c.reset}`
115
+ );
116
+ lines.push(sep);
117
+
118
+ // Per-account rows
119
+ for (const w of workers) {
120
+ const status = w.lastStatus || 'idle';
121
+ const statusColor = w.running ? (w.busy ? c.yellow : c.green) : c.red;
122
+ const dot = w.running ? (w.busy ? '◉' : '●') : '○';
123
+ const bal = w.stats.balance > 0 ? `${c.dim}⏣${c.reset} ${c.white}${formatCoins(w.stats.balance)}${c.reset}` : `${c.dim}⏣ ---${c.reset}`;
124
+ const earned = w.stats.coins > 0 ? `${c.green}+${formatCoins(w.stats.coins)}${c.reset}` : `${c.dim}+0${c.reset}`;
125
+ const cmds = `${c.dim}${w.stats.commands}cmd${c.reset}`;
126
+ const name = `${w.color}${c.bold}${(w.username || 'Account').substring(0, 14).padEnd(14)}${c.reset}`;
127
+
128
+ lines.push(
129
+ ` ${statusColor}${dot}${c.reset} ${name} ${bal} ${earned} ${cmds} ${c.dim}→${c.reset} ${status.substring(0, 35)}`
130
+ );
131
+ }
132
+ lines.push(sep);
133
+
134
+ // Scrolling log section (last 6 events)
135
+ for (const entry of recentLogs) {
136
+ lines.push(` ${c.dim}${entry}${c.reset}`);
137
+ }
138
+ if (recentLogs.length > 0) lines.push(sep);
139
+
140
+ // Move cursor up to overwrite previous dashboard
141
+ if (dashboardLines > 0) {
142
+ process.stdout.write(c.cursorUp(dashboardLines));
143
+ }
144
+ for (const line of lines) {
145
+ process.stdout.write(c.clearLine + '\r' + line + '\n');
146
+ }
147
+ dashboardLines = lines.length;
148
+ }
149
+
22
150
  function log(type, msg, label) {
23
- const time = new Date().toLocaleTimeString();
24
- const prefix = {
25
- info: `${c.cyan}ℹ${c.reset}`,
26
- success: `${c.green}✓${c.reset}`,
27
- error: `${c.red}✗${c.reset}`,
28
- warn: `${c.yellow}⚠${c.reset}`,
29
- cmd: `${c.magenta}▸${c.reset}`,
151
+ const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
152
+ const icons = {
153
+ info: `│`, success: `✓`, error: `✗`, warn: `!`,
154
+ cmd: `▸`, coin: `$`, buy: `♦`, bal: `◈`, debug: `⊙`,
30
155
  };
31
- const tag = label ? `${c.bold}[${label}]${c.reset} ` : '';
32
- console.log(` ${c.dim}${time}${c.reset} ${prefix[type] || prefix.info} ${tag}${msg}`);
156
+ const tag = label ? `${label} ` : '';
157
+ const stripped = msg.replace(/\x1b\[[0-9;]*m/g, ''); // Strip ANSI for log buffer
158
+ if (dashboardStarted) {
159
+ // Add to scrolling log buffer
160
+ recentLogs.push(`${time} ${icons[type] || '│'} ${tag}${stripped}`.substring(0, 80));
161
+ while (recentLogs.length > MAX_LOGS) recentLogs.shift();
162
+ renderDashboard();
163
+ } else {
164
+ const colorIcons = {
165
+ info: `${c.cyan}│${c.reset}`, success: `${c.green}✓${c.reset}`,
166
+ error: `${c.red}✗${c.reset}`, warn: `${c.yellow}!${c.reset}`,
167
+ cmd: `${c.magenta}▸${c.reset}`, coin: `${c.yellow}$${c.reset}`,
168
+ buy: `${c.blue}♦${c.reset}`, bal: `${c.green}◈${c.reset}`,
169
+ debug: `${c.dim}⊙${c.reset}`,
170
+ };
171
+ console.log(` ${c.dim}${time}${c.reset} ${colorIcons[type] || colorIcons.info} ${tag}${msg}`);
172
+ }
33
173
  }
34
174
 
35
175
  async function fetchConfig() {
@@ -38,178 +178,778 @@ async function fetchConfig() {
38
178
  headers: { Authorization: `Bearer ${API_KEY}` },
39
179
  });
40
180
  const data = await res.json();
41
- if (data.error) {
42
- log('error', `Config fetch failed: ${data.error}`);
43
- return null;
44
- }
181
+ if (data.error) { log('error', `Config fetch failed: ${data.error}`); return null; }
45
182
  return data;
46
- } catch (err) {
47
- log('error', `Cannot reach API: ${err.message}`);
48
- return null;
49
- }
183
+ } catch (err) { log('error', `Cannot reach API: ${err.message}`); return null; }
50
184
  }
51
185
 
52
- async function sendLog(command, response, status) {
186
+ async function sendLog(accountName, command, response, status) {
53
187
  try {
54
188
  await fetch(`${API_URL}/api/grinder/log`, {
55
189
  method: 'POST',
56
- headers: {
57
- Authorization: `Bearer ${API_KEY}`,
58
- 'Content-Type': 'application/json',
59
- },
60
- body: JSON.stringify({ command, response, status }),
190
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
191
+ body: JSON.stringify({ account_name: accountName, command, response, status }),
61
192
  });
62
- } catch {
63
- // silent fail for logging
64
- }
193
+ } catch { /* silent */ }
65
194
  }
66
195
 
67
196
  function randomDelay(min, max) {
68
- const ms = (Math.random() * (max - min) + min) * 1000;
69
- return new Promise((resolve) => setTimeout(resolve, ms));
197
+ return new Promise((r) => setTimeout(r, (Math.random() * (max - min) + min) * 1000));
198
+ }
199
+ function humanDelay(min = 300, max = 800) {
200
+ return new Promise((r) => setTimeout(r, min + Math.random() * (max - min)));
201
+ }
202
+ function safeParseJSON(str, fallback = []) {
203
+ try { return JSON.parse(str || '[]'); } catch { return fallback; }
70
204
  }
71
205
 
72
- function humanDelay() {
73
- return new Promise((resolve) => setTimeout(resolve, 200 + Math.random() * 600));
206
+ // ── Coin Parser ──────────────────────────────────────────────
207
+ function parseCoins(text) {
208
+ if (!text) return 0;
209
+ const patterns = [
210
+ /[⏣💰]\s*[\d,]+/g,
211
+ /\+\s*[\d,]+\s*coins?/gi,
212
+ /\*\*([\d,]+)\*\*/g,
213
+ /take\s*[⏣💰]?\s*([\d,]+)/gi,
214
+ /earned\s*[⏣💰]?\s*([\d,]+)/gi,
215
+ /found\s*[⏣💰]?\s*([\d,]+)/gi,
216
+ /got\s*[⏣💰]?\s*([\d,]+)/gi,
217
+ /won\s*[⏣💰]?\s*([\d,]+)/gi,
218
+ /gained\s*[⏣💰]?\s*([\d,]+)/gi,
219
+ ];
220
+ let total = 0;
221
+ for (const pat of patterns) {
222
+ const matches = text.match(pat);
223
+ if (matches) {
224
+ for (const m of matches) {
225
+ const numStr = m.replace(/[^\d]/g, '');
226
+ if (numStr) total = Math.max(total, parseInt(numStr));
227
+ }
228
+ }
229
+ }
230
+ return total;
74
231
  }
75
232
 
76
- function safeParseJSON(str, fallback) {
77
- try {
78
- return JSON.parse(str || '[]');
79
- } catch {
80
- return fallback;
233
+ function getFullText(msg) {
234
+ let text = msg.content || '';
235
+ if (msg.embeds) {
236
+ for (const embed of msg.embeds) {
237
+ if (embed.title) text += ' ' + embed.title;
238
+ if (embed.description) text += ' ' + embed.description;
239
+ if (embed.fields) {
240
+ for (const f of embed.fields) {
241
+ text += ' ' + (f.name || '') + ' ' + (f.value || '');
242
+ }
243
+ }
244
+ if (embed.footer?.text) text += ' ' + embed.footer.text;
245
+ }
81
246
  }
247
+ return text;
82
248
  }
83
249
 
84
- // ── Worker: one per Discord account ──────────────────────────────
250
+ function getAllButtons(msg) {
251
+ const buttons = [];
252
+ if (msg.components) {
253
+ for (const row of msg.components) {
254
+ if (row.components) {
255
+ for (const comp of row.components) {
256
+ if (comp.type === 2) buttons.push(comp);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ return buttons;
262
+ }
263
+
264
+ function getAllSelectMenus(msg) {
265
+ const menus = [];
266
+ if (msg.components) {
267
+ for (const row of msg.components) {
268
+ if (row.components) {
269
+ for (const comp of row.components) {
270
+ if (comp.type === 3) menus.push(comp);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ return menus;
276
+ }
277
+
278
+ function pickSafeButton(buttons, safeList) {
279
+ if (!buttons || buttons.length === 0) return null;
280
+ if (safeList && safeList.length > 0) {
281
+ for (const btn of buttons) {
282
+ const label = (btn.label || '').toLowerCase();
283
+ if (safeList.some((s) => label.includes(s.toLowerCase()))) return btn;
284
+ }
285
+ }
286
+ for (const btn of buttons) {
287
+ const label = (btn.label || '').toLowerCase();
288
+ if (SAFE_SEARCH_LOCATIONS.some((s) => label.includes(s))) return btn;
289
+ if (SAFE_CRIME_OPTIONS.some((s) => label.includes(s))) return btn;
290
+ }
291
+ const clickable = buttons.filter((b) => !b.disabled);
292
+ return clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
293
+ }
294
+
295
+ // Debug: log all embed/component data for a message
296
+ function debugMessage(msg, label) {
297
+ if (!msg) return;
298
+ const text = getFullText(msg);
299
+ log('debug', `${label || 'MSG'}: ${c.dim}${text.substring(0, 120)}${c.reset}`);
300
+ const buttons = getAllButtons(msg);
301
+ if (buttons.length > 0) {
302
+ log('debug', ` Buttons: ${buttons.map(b => `[${b.label || b.emoji?.name || '?'}${b.disabled ? ' (disabled)' : ''}]`).join(' ')}`);
303
+ }
304
+ const menus = getAllSelectMenus(msg);
305
+ if (menus.length > 0) {
306
+ for (const m of menus) {
307
+ log('debug', ` SelectMenu: ${(m.options || []).map(o => o.label).join(', ')}`);
308
+ }
309
+ }
310
+ }
311
+
312
+ // ══════════════════════════════════════════════════════════════
313
+ // ═ Worker: one per Discord account
314
+ // ══════════════════════════════════════════════════════════════
85
315
 
86
316
  class AccountWorker {
87
317
  constructor(account, idx) {
88
318
  this.account = account;
89
- this.label = account.label || `Account ${idx + 1}`;
90
- this.color = ACCOUNT_COLORS[idx % ACCOUNT_COLORS.length];
319
+ this.idx = idx;
320
+ this.color = WORKER_COLORS[idx % WORKER_COLORS.length];
91
321
  this.client = new Client();
92
322
  this.channel = null;
93
323
  this.running = false;
324
+ this.busy = false;
325
+ this.username = account.label || `Account ${idx + 1}`;
326
+ this.tickTimeout = null;
327
+ this.stats = { coins: 0, commands: 0, successes: 0, errors: 0, balance: 0 };
328
+ this.lastRunTime = {};
329
+ this.cycleCount = 0;
330
+ this.lastCommandRun = 0;
94
331
  }
95
332
 
96
- log(type, msg) {
97
- log(type, msg, `${this.color}${this.label}${c.reset}`);
333
+ get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
334
+ log(type, msg) { log(type, msg, this.tag); }
335
+
336
+ // Update the live dashboard status for this worker
337
+ setStatus(text) {
338
+ this.lastStatus = text;
339
+ renderDashboard();
98
340
  }
99
341
 
100
342
  waitForDankMemer(timeout = 15000) {
101
343
  return new Promise((resolve) => {
102
344
  const timer = setTimeout(() => {
103
345
  this.client.removeListener('messageCreate', handler);
346
+ this.client.removeListener('messageUpdate', updateHandler);
104
347
  resolve(null);
105
348
  }, timeout);
106
-
107
349
  const self = this;
108
350
  function handler(msg) {
109
- if (msg.author.id === '270904126974590976' && msg.channel.id === self.channel.id) {
351
+ if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
110
352
  clearTimeout(timer);
111
353
  self.client.removeListener('messageCreate', handler);
354
+ self.client.removeListener('messageUpdate', updateHandler);
112
355
  resolve(msg);
113
356
  }
114
357
  }
115
-
358
+ function updateHandler(oldMsg, newMsg) {
359
+ if (newMsg.author?.id === DANK_MEMER_ID && newMsg.channel?.id === self.channel.id) {
360
+ clearTimeout(timer);
361
+ self.client.removeListener('messageCreate', handler);
362
+ self.client.removeListener('messageUpdate', updateHandler);
363
+ resolve(newMsg);
364
+ }
365
+ }
116
366
  this.client.on('messageCreate', handler);
367
+ this.client.on('messageUpdate', updateHandler);
117
368
  });
118
369
  }
119
370
 
120
- async handleInteraction(msg) {
121
- if (!msg) return null;
122
-
123
- if (msg.components && msg.components.length > 0) {
124
- for (const row of msg.components) {
125
- if (row.components) {
126
- for (const component of row.components) {
127
- if (component.type === 2) {
128
- const label = component.label?.toLowerCase() || '';
129
- const searchAnswers = safeParseJSON(this.account.search_answers, []);
130
- const crimeAnswers = safeParseJSON(this.account.crime_answers, []);
131
- const allSafe = [...searchAnswers, ...crimeAnswers];
132
-
133
- if (allSafe.length > 0) {
134
- const match = allSafe.find((a) => label.includes(a.toLowerCase()));
135
- if (match) {
136
- await humanDelay();
137
- try {
138
- await component.click();
139
- return `Clicked: ${label}`;
140
- } catch {
141
- return null;
142
- }
143
- }
144
- }
145
- }
371
+ // Wait specifically for message edits (Dank Memer often edits messages)
372
+ waitForMessageUpdate(msgId, timeout = 10000) {
373
+ return new Promise((resolve) => {
374
+ const timer = setTimeout(() => {
375
+ this.client.removeListener('messageUpdate', handler);
376
+ resolve(null);
377
+ }, timeout);
378
+ const self = this;
379
+ function handler(oldMsg, newMsg) {
380
+ if (newMsg.id === msgId && newMsg.channel?.id === self.channel.id) {
381
+ clearTimeout(timer);
382
+ self.client.removeListener('messageUpdate', handler);
383
+ resolve(newMsg);
384
+ }
385
+ }
386
+ this.client.on('messageUpdate', handler);
387
+ });
388
+ }
389
+
390
+ // ── Needs Item Detection ────────────────────────────────────
391
+ needsItem(text) {
392
+ const lower = text.toLowerCase();
393
+ if (lower.includes("don't have a shovel") || (lower.includes('need') && lower.includes('shovel')) || lower.includes('you need a shovel'))
394
+ return 'shovel';
395
+ if (lower.includes("don't have a fishing") || lower.includes('need a fishing') || lower.includes('you need a fishing pole'))
396
+ return 'fishing pole';
397
+ if (lower.includes("don't have a hunting rifle") || lower.includes('need a hunting rifle') || lower.includes('you need a rifle'))
398
+ return 'hunting rifle';
399
+ return null;
400
+ }
401
+
402
+ async buyItem(itemName, quantity = 10) {
403
+ const MAX_RETRIES = 3;
404
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
405
+ this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
406
+ if (this.account.use_slash) {
407
+ await this.channel.sendSlash(DANK_MEMER_ID, 'shop', 'view').catch(() => this.channel.send('pls shop view'));
408
+ } else {
409
+ await this.channel.send('pls shop view');
410
+ }
411
+ let response = await this.waitForDankMemer(10000);
412
+ if (!response) {
413
+ this.log('warn', 'No response to shop view command.');
414
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
415
+ return false;
416
+ }
417
+
418
+ const responseText = getFullText(response).toLowerCase();
419
+ const hasShopComponents = (response.components || []).some(row =>
420
+ (row.components || []).some(comp => comp.type === 3 || (comp.type === 2 && comp.label && comp.label.toLowerCase().includes('buy')))
421
+ );
422
+
423
+ if (!hasShopComponents && (responseText.includes('lucky') || responseText.includes('event') || responseText.includes('for the rest of the day'))) {
424
+ this.log('warn', 'Got event response instead of shop. Retrying...');
425
+ await humanDelay(3000, 5000);
426
+ continue;
427
+ }
428
+ if (!hasShopComponents && responseText.includes('shop')) {
429
+ const shopUI = await this.waitForDankMemer(8000);
430
+ if (shopUI) response = shopUI;
431
+ }
432
+
433
+ // Navigate to Coin Shop
434
+ let coinShopMenuId = null;
435
+ let coinShopOption = null;
436
+ for (const row of response.components || []) {
437
+ for (const comp of row.components || []) {
438
+ if (comp.type === 3) {
439
+ const opt = (comp.options || []).find(o => o.label && o.label.includes('Coin Shop'));
440
+ if (opt) { coinShopMenuId = comp.customId; coinShopOption = opt; }
146
441
  }
442
+ }
443
+ }
147
444
 
148
- const buttons = row.components.filter((comp) => comp.type === 2 && !comp.disabled);
149
- if (buttons.length > 0) {
150
- const btn = buttons[Math.floor(Math.random() * buttons.length)];
151
- await humanDelay();
152
- try {
153
- await btn.click();
154
- return `Clicked: ${btn.label || 'button'}`;
155
- } catch {
156
- return null;
157
- }
445
+ if (coinShopMenuId && coinShopOption) {
446
+ this.log('buy', 'Navigating to Coin Shop...');
447
+ try {
448
+ await response.selectMenu(coinShopMenuId, [coinShopOption.value]);
449
+ const updatedMsg = await this.waitForDankMemer(8000);
450
+ if (updatedMsg) response = updatedMsg;
451
+ } catch (e) {
452
+ this.log('error', `Failed to open Coin Shop: ${e.message}`);
453
+ }
454
+ }
455
+ await humanDelay(1000, 2000);
456
+
457
+ // Find Buy button
458
+ let buyBtn = null;
459
+ const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '');
460
+ for (const row of response.components || []) {
461
+ for (const comp of row.components || []) {
462
+ if (comp.type === 2 && comp.label && comp.label.toLowerCase().includes(searchName)) {
463
+ buyBtn = comp; break;
158
464
  }
159
465
  }
466
+ if (buyBtn) break;
467
+ }
468
+
469
+ if (!buyBtn) {
470
+ this.log('warn', `Could not find Buy button for ${itemName} (attempt ${attempt})`);
471
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
472
+ return false;
473
+ }
474
+
475
+ this.log('buy', `Clicking Buy ${itemName}...`);
476
+ try { await buyBtn.click(); } catch (e) {
477
+ this.log('error', `Buy click failed: ${e.message}`);
478
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
479
+ return false;
480
+ }
481
+
482
+ // Handle Modal
483
+ const modal = await new Promise((resolve) => {
484
+ const timer = setTimeout(() => resolve(null), 8000);
485
+ const handler = (m) => {
486
+ clearTimeout(timer);
487
+ this.client.removeListener('interactionModalCreate', handler);
488
+ resolve(m);
489
+ };
490
+ this.client.on('interactionModalCreate', handler);
491
+ });
492
+
493
+ if (modal) {
494
+ this.log('buy', `Submitting quantity ${c.bold}${quantity}${c.reset} in modal...`);
495
+ try {
496
+ const quantityInputId = modal.components[0].components[0].customId;
497
+ await fetch('https://discord.com/api/v9/interactions', {
498
+ method: 'POST',
499
+ headers: { 'Authorization': this.client.token, 'Content-Type': 'application/json' },
500
+ body: JSON.stringify({
501
+ type: 5, application_id: modal.applicationId,
502
+ channel_id: this.channel.id, guild_id: this.channel.guild?.id,
503
+ data: {
504
+ id: modal.id, custom_id: modal.customId,
505
+ components: [{ type: 1, components: [{ type: 4, custom_id: quantityInputId, value: String(quantity) }] }]
506
+ },
507
+ session_id: this.client.sessionId || "dummy_session",
508
+ nonce: Date.now().toString()
509
+ })
510
+ });
511
+ } catch (e) {
512
+ this.log('error', `Modal submit failed: ${e.message}`);
513
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
514
+ return false;
515
+ }
516
+ } else {
517
+ this.log('warn', 'No modal appeared after clicking buy.');
518
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
519
+ return false;
520
+ }
521
+
522
+ const confirmMsg = await this.waitForDankMemer(8000);
523
+ if (confirmMsg) {
524
+ const text = getFullText(confirmMsg).toLowerCase();
525
+ if (text.includes('bought') || text.includes('purchased') || text.includes('success')) {
526
+ this.log('success', `Bought ${c.bold}${quantity}x ${itemName}${c.reset}!`);
527
+ return true;
528
+ }
529
+ if (text.includes('not enough') || text.includes("can't afford") || text.includes('insufficient')) {
530
+ this.log('warn', `Not enough coins to buy ${itemName}.`);
531
+ return false;
532
+ }
533
+ }
534
+ this.log('success', `Submitted purchase for ${quantity}x ${itemName}.`);
535
+ return true;
536
+ }
537
+ this.log('error', `Failed to buy ${itemName} after ${MAX_RETRIES} attempts.`);
538
+ return false;
539
+ }
540
+
541
+ // ── Check Balance ───────────────────────────────────────────
542
+ async checkBalance() {
543
+ const prefix = this.account.use_slash ? '/' : 'pls';
544
+ await this.channel.send(`${prefix} bal`);
545
+ const response = await this.waitForDankMemer(10000);
546
+ if (response) {
547
+ const text = getFullText(response);
548
+ const walletMatch = text.match(/wallet[:\s]*[⏣💰]?\s*([\d,]+)/i);
549
+ if (walletMatch) {
550
+ this.stats.balance = parseInt(walletMatch[1].replace(/,/g, ''));
551
+ this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${this.stats.balance.toLocaleString()}${c.reset}`);
552
+ try {
553
+ await fetch(`${API_URL}/api/grinder/status`, {
554
+ method: 'POST',
555
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
556
+ body: JSON.stringify({ account_id: this.account.id, balance: this.stats.balance }),
557
+ });
558
+ } catch { /* silent */ }
559
+ }
560
+ }
561
+ }
562
+
563
+ // ══════════════════════════════════════════════════════════════
564
+ // ═ COMMAND HANDLERS
565
+ // ══════════════════════════════════════════════════════════════
566
+
567
+ async handleSearch(response) {
568
+ if (!response) return null;
569
+ debugMessage(response, 'SEARCH');
570
+ const buttons = getAllButtons(response);
571
+ if (buttons.length === 0) return getFullText(response).substring(0, 80);
572
+ const userSafe = safeParseJSON(this.account.search_answers, []);
573
+ const btn = pickSafeButton(buttons, userSafe);
574
+ if (btn) {
575
+ await humanDelay();
576
+ try {
577
+ await btn.click();
578
+ const followUp = await this.waitForDankMemer(10000);
579
+ if (followUp) {
580
+ const text = getFullText(followUp);
581
+ const coins = parseCoins(text);
582
+ if (coins > 0) { this.stats.coins += coins; return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
583
+ if (text.toLowerCase().includes('nothing')) return `${btn.label} → found nothing`;
584
+ return `${btn.label} → done`;
585
+ }
586
+ return `Clicked: ${btn.label}`;
587
+ } catch { return null; }
588
+ }
589
+ return 'No safe option found';
590
+ }
591
+
592
+ async handleCrime(response) {
593
+ if (!response) return null;
594
+ debugMessage(response, 'CRIME');
595
+ const buttons = getAllButtons(response);
596
+ if (buttons.length === 0) return getFullText(response).substring(0, 80);
597
+ const userSafe = safeParseJSON(this.account.crime_answers, []);
598
+ const btn = pickSafeButton(buttons, userSafe);
599
+ if (btn) {
600
+ await humanDelay();
601
+ try {
602
+ await btn.click();
603
+ const followUp = await this.waitForDankMemer(10000);
604
+ if (followUp) {
605
+ const text = getFullText(followUp);
606
+ const coins = parseCoins(text);
607
+ if (coins > 0) { this.stats.coins += coins; return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
608
+ return `${btn.label} → done`;
609
+ }
610
+ return `Clicked: ${btn.label}`;
611
+ } catch { return null; }
612
+ }
613
+ return 'No safe option found';
614
+ }
615
+
616
+ async handleHighLow(response) {
617
+ if (!response) return null;
618
+ debugMessage(response, 'HIGHLOW');
619
+ const text = getFullText(response);
620
+ const match = text.match(/number.*?(\d+)/i) || text.match(/(\d+)/);
621
+ const buttons = getAllButtons(response);
622
+ if (match && buttons.length >= 2) {
623
+ const num = parseInt(match[1]);
624
+ await humanDelay();
625
+ let targetBtn;
626
+ if (num > 50) targetBtn = buttons.find((b) => (b.label || '').toLowerCase().includes('lower')) || buttons[1];
627
+ else if (num < 50) targetBtn = buttons.find((b) => (b.label || '').toLowerCase().includes('higher')) || buttons[0];
628
+ else {
629
+ const jackpot = buttons.find((b) => (b.label || '').toLowerCase().includes('jackpot'));
630
+ targetBtn = jackpot || buttons[Math.floor(Math.random() * 2)];
160
631
  }
632
+ try {
633
+ await targetBtn.click();
634
+ const followUp = await this.waitForDankMemer(10000);
635
+ if (followUp) {
636
+ const ftText = getFullText(followUp);
637
+ const coins = parseCoins(ftText);
638
+ if (coins > 0) { this.stats.coins += coins; return `${num} → ${targetBtn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
639
+ const moreButtons = getAllButtons(followUp);
640
+ if (moreButtons.length >= 2) return await this.handleHighLow(followUp);
641
+ return `${num} → ${targetBtn.label}`;
642
+ }
643
+ return `${num} → ${targetBtn.label}`;
644
+ } catch { return null; }
161
645
  }
646
+ if (buttons.length > 0) {
647
+ const btn = buttons[Math.floor(Math.random() * buttons.length)];
648
+ await humanDelay();
649
+ try { await btn.click(); } catch {}
650
+ return `Clicked: ${btn.label || 'button'}`;
651
+ }
652
+ return null;
653
+ }
162
654
 
163
- return msg.content?.substring(0, 100) || 'Response received';
655
+ async handleScratch(response) {
656
+ if (!response) return null;
657
+ debugMessage(response, 'SCRATCH');
658
+ const buttons = getAllButtons(response);
659
+ if (buttons.length === 0) {
660
+ const text = getFullText(response);
661
+ const coins = parseCoins(text);
662
+ if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
663
+ return text.substring(0, 60) || 'done';
664
+ }
665
+ let lastResponse = response;
666
+ for (let i = 0; i < Math.min(buttons.length, 9); i++) {
667
+ const btn = buttons[i];
668
+ if (btn && !btn.disabled) {
669
+ await humanDelay(300, 700);
670
+ try {
671
+ await btn.click();
672
+ const followUp = await this.waitForDankMemer(5000);
673
+ if (followUp) lastResponse = followUp;
674
+ } catch { break; }
675
+ }
676
+ }
677
+ const finalText = getFullText(lastResponse);
678
+ const coins = parseCoins(finalText);
679
+ if (coins > 0) { this.stats.coins += coins; return `scratch → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
680
+ return 'scratch done';
164
681
  }
165
682
 
166
- async handleHighLow(msg) {
167
- if (!msg) return null;
683
+ async handleAdventure(response) {
684
+ if (!response) return null;
685
+ debugMessage(response, 'ADVENTURE');
686
+ let current = response;
687
+ let rounds = 0;
688
+ const MAX_ROUNDS = 15;
689
+ while (current && rounds < MAX_ROUNDS) {
690
+ const buttons = getAllButtons(current);
691
+ if (buttons.length === 0) break;
692
+ const safeButtons = buttons.filter((b) => !b.disabled && b.style !== 4);
693
+ const btn = safeButtons.length > 0 ? safeButtons[Math.floor(Math.random() * safeButtons.length)] : buttons.find((b) => !b.disabled);
694
+ if (!btn) break;
695
+ await humanDelay(500, 1500);
696
+ try {
697
+ await btn.click();
698
+ const followUp = await this.waitForDankMemer(10000);
699
+ if (followUp) { current = followUp; rounds++; } else break;
700
+ } catch { break; }
701
+ }
702
+ const finalText = getFullText(current);
703
+ // Parse next adventure time from footer
704
+ const nextMatch = finalText.match(/next adventure.*?(\d+)/i);
705
+ if (nextMatch) {
706
+ const nextSec = parseInt(nextMatch[1]) * 60;
707
+ this.log('info', `Next adventure in ${c.yellow}${nextMatch[1]}m${c.reset}`);
708
+ await this.setCooldown('adventure', nextSec);
709
+ }
710
+ const coins = parseCoins(finalText);
711
+ if (coins > 0) { this.stats.coins += coins; return `adventure (${rounds} rounds) → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
712
+ return `adventure done (${rounds} rounds)`;
713
+ }
168
714
 
169
- if (msg.embeds && msg.embeds.length > 0) {
170
- const embed = msg.embeds[0];
171
- const desc = embed.description || '';
172
- const match = desc.match(/(\d+)/);
173
- if (match) {
174
- const num = parseInt(match[1]);
175
- const buttons = [];
176
- if (msg.components) {
177
- for (const row of msg.components) {
178
- if (row.components) {
179
- for (const comp of row.components) {
180
- if (comp.type === 2) buttons.push(comp);
715
+ // ── BlackJack Handler ─────────────────────────────────────
716
+ async handleBlackjack(response) {
717
+ if (!response) return null;
718
+ debugMessage(response, 'BLACKJACK');
719
+ let current = response;
720
+ const MAX_ROUNDS = 10;
721
+ for (let i = 0; i < MAX_ROUNDS; i++) {
722
+ const text = getFullText(current);
723
+ const buttons = getAllButtons(current);
724
+ if (buttons.length === 0) break;
725
+ // Parse player total from embed - look for patterns like "Total: 15" or "Value: **15**"
726
+ const totalMatch = text.match(/total[:\s]*\**(\d+)\**/i) || text.match(/value[:\s]*\**(\d+)\**/i);
727
+ const playerTotal = totalMatch ? parseInt(totalMatch[1]) : 0;
728
+ let targetBtn;
729
+ if (playerTotal >= 17 || playerTotal === 0) {
730
+ targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('stand')) || buttons[1];
731
+ } else {
732
+ targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('hit')) || buttons[0];
733
+ }
734
+ if (!targetBtn || targetBtn.disabled) break;
735
+ await humanDelay(500, 1200);
736
+ try {
737
+ await targetBtn.click();
738
+ const followUp = await this.waitForDankMemer(8000);
739
+ if (followUp) {
740
+ current = followUp;
741
+ const fButtons = getAllButtons(current);
742
+ if (fButtons.length === 0 || fButtons.every(b => b.disabled)) break;
743
+ } else break;
744
+ } catch { break; }
745
+ }
746
+ const finalText = getFullText(current);
747
+ const coins = parseCoins(finalText);
748
+ if (coins > 0) { this.stats.coins += coins; return `blackjack → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
749
+ if (finalText.toLowerCase().includes('won')) return 'blackjack → won';
750
+ if (finalText.toLowerCase().includes('lost') || finalText.toLowerCase().includes('bust')) return 'blackjack → lost';
751
+ return 'blackjack done';
752
+ }
753
+
754
+ // ── Trivia Handler ────────────────────────────────────────
755
+ async handleTrivia(response) {
756
+ if (!response) return null;
757
+ debugMessage(response, 'TRIVIA');
758
+ const buttons = getAllButtons(response);
759
+ if (buttons.length === 0) return getFullText(response).substring(0, 80);
760
+ // Pick a random answer (we don't have a trivia DB - random has ~25% chance)
761
+ const clickable = buttons.filter(b => !b.disabled);
762
+ const btn = clickable[Math.floor(Math.random() * clickable.length)];
763
+ if (btn) {
764
+ await humanDelay(1000, 3000);
765
+ try {
766
+ await btn.click();
767
+ const followUp = await this.waitForDankMemer(8000);
768
+ if (followUp) {
769
+ const text = getFullText(followUp);
770
+ const coins = parseCoins(text);
771
+ if (coins > 0) { this.stats.coins += coins; return `trivia → ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
772
+ if (text.toLowerCase().includes('correct')) return `trivia → ${btn.label} → correct`;
773
+ return `trivia → ${btn.label} → wrong`;
774
+ }
775
+ } catch {}
776
+ }
777
+ return 'trivia done';
778
+ }
779
+
780
+ // ── Work Shift Handler ────────────────────────────────────
781
+ async handleWorkShift(response) {
782
+ if (!response) return null;
783
+ debugMessage(response, 'WORK');
784
+ let current = response;
785
+ const MAX_ROUNDS = 5;
786
+ for (let i = 0; i < MAX_ROUNDS; i++) {
787
+ const buttons = getAllButtons(current);
788
+ const text = getFullText(current);
789
+ // Work shift mini-games: word scramble, memory, typing, etc.
790
+ // Try to click any available non-disabled button
791
+ if (buttons.length === 0) break;
792
+ const clickable = buttons.filter(b => !b.disabled);
793
+ if (clickable.length === 0) break;
794
+ // For word-type games, look for the correct button
795
+ const btn = clickable[0]; // Just pick first available
796
+ await humanDelay(500, 1500);
797
+ try {
798
+ await btn.click();
799
+ const followUp = await this.waitForDankMemer(8000);
800
+ if (followUp) { current = followUp; } else break;
801
+ } catch { break; }
802
+ }
803
+ const finalText = getFullText(current);
804
+ const coins = parseCoins(finalText);
805
+ if (coins > 0) { this.stats.coins += coins; return `work → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
806
+ return 'work done';
807
+ }
808
+
809
+ // ── Generic Command Handler (beg, hunt, dig, farm, tidy, etc.) ──
810
+ async handleGenericCommand(response) {
811
+ if (!response) return null;
812
+ debugMessage(response, 'GENERIC');
813
+ const text = getFullText(response);
814
+ const coins = parseCoins(text);
815
+ const neededItem = this.needsItem(text);
816
+ if (neededItem) {
817
+ this.log('warn', `Missing ${c.bold}${neededItem}${c.reset} — auto-buying...`);
818
+ const bought = await this.buyItem(neededItem);
819
+ if (bought) return `auto-bought ${neededItem}`;
820
+ return `need ${neededItem} (couldn't buy)`;
821
+ }
822
+ // Handle buttons if present (e.g., postmemes, farm choices)
823
+ const buttons = getAllButtons(response);
824
+ if (buttons.length > 0) {
825
+ const btn = buttons.find((b) => !b.disabled) || buttons[0];
826
+ if (btn) {
827
+ await humanDelay();
828
+ try {
829
+ await btn.click();
830
+ const followUp = await this.waitForDankMemer(8000);
831
+ if (followUp) {
832
+ const fText = getFullText(followUp);
833
+ const fCoins = parseCoins(fText);
834
+ if (fCoins > 0) { this.stats.coins += fCoins; return `${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
835
+ // Check if followUp also has buttons (multi-step)
836
+ const nextButtons = getAllButtons(followUp);
837
+ if (nextButtons.length > 0) {
838
+ const nextBtn = nextButtons.find(b => !b.disabled);
839
+ if (nextBtn) {
840
+ await humanDelay();
841
+ try { await nextBtn.click(); } catch {}
181
842
  }
182
843
  }
183
844
  }
184
- }
185
-
186
- if (buttons.length >= 2) {
187
- await humanDelay();
188
- let targetBtn;
189
- if (num > 50) {
190
- targetBtn = buttons.find((b) => b.label?.toLowerCase().includes('lower')) || buttons[1];
191
- } else if (num < 50) {
192
- targetBtn = buttons.find((b) => b.label?.toLowerCase().includes('higher')) || buttons[0];
193
- } else {
194
- targetBtn = buttons[Math.floor(Math.random() * 2)];
845
+ } catch {}
846
+ }
847
+ }
848
+ // Handle select menus (some commands use dropdowns)
849
+ const menus = getAllSelectMenus(response);
850
+ if (menus.length > 0) {
851
+ const menu = menus[0];
852
+ const options = menu.options || [];
853
+ if (options.length > 0) {
854
+ const opt = options[Math.floor(Math.random() * options.length)];
855
+ try {
856
+ await response.selectMenu(menu.customId, [opt.value]);
857
+ const followUp = await this.waitForDankMemer(8000);
858
+ if (followUp) {
859
+ const fText = getFullText(followUp);
860
+ const fCoins = parseCoins(fText);
861
+ if (fCoins > 0) { this.stats.coins += fCoins; return `${opt.label} → ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
195
862
  }
863
+ } catch {}
864
+ }
865
+ }
866
+ if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
867
+ return text.substring(0, 60) || 'done';
868
+ }
196
869
 
197
- try {
198
- await targetBtn.click();
199
- return `HL: number was ${num}, clicked ${targetBtn.label}`;
200
- } catch {
201
- return null;
870
+ // ── Simple Gambling (cointoss, roulette, slots, snakeeyes) ──
871
+ async handleSimpleGamble(response) {
872
+ if (!response) return null;
873
+ debugMessage(response, 'GAMBLE');
874
+ const text = getFullText(response);
875
+ const coins = parseCoins(text);
876
+ // Check buttons (some gambling has buttons)
877
+ const buttons = getAllButtons(response);
878
+ if (buttons.length > 0) {
879
+ const btn = buttons.find(b => !b.disabled);
880
+ if (btn) {
881
+ await humanDelay();
882
+ try {
883
+ await btn.click();
884
+ const followUp = await this.waitForDankMemer(8000);
885
+ if (followUp) {
886
+ const fText = getFullText(followUp);
887
+ const fCoins = parseCoins(fText);
888
+ if (fCoins > 0) { this.stats.coins += fCoins; return `${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
889
+ if (fText.toLowerCase().includes('won')) return `${c.green}won${c.reset}`;
890
+ if (fText.toLowerCase().includes('lost')) return `${c.red}lost${c.reset}`;
202
891
  }
203
- }
892
+ } catch {}
204
893
  }
205
894
  }
895
+ if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
896
+ if (text.toLowerCase().includes('won')) return `${c.green}won${c.reset}`;
897
+ if (text.toLowerCase().includes('lost')) return `${c.red}lost${c.reset}`;
898
+ return text.substring(0, 60) || 'done';
899
+ }
206
900
 
207
- return null;
901
+ // ── Deposit Handler ────────────────────────────────────────
902
+ async handleDeposit(response) {
903
+ if (!response) return null;
904
+ const text = getFullText(response);
905
+ const coins = parseCoins(text);
906
+ if (text.toLowerCase().includes('deposited')) {
907
+ return `deposited ${c.green}⏣ ${coins.toLocaleString()}${c.reset}`;
908
+ }
909
+ return text.substring(0, 60) || 'deposit done';
208
910
  }
209
911
 
912
+ // ── Alert Handler ─────────────────────────────────────────
913
+ async handleAlert(response) {
914
+ if (!response) return null;
915
+ debugMessage(response, 'ALERT');
916
+ const buttons = getAllButtons(response);
917
+ // Click any dismiss/accept/ok button
918
+ const btn = buttons.find(b => !b.disabled && ['ok', 'dismiss', 'accept', 'got it', 'continue'].some(s => (b.label || '').toLowerCase().includes(s)));
919
+ if (btn) {
920
+ await humanDelay();
921
+ try { await btn.click(); } catch {}
922
+ return `alert dismissed`;
923
+ }
924
+ if (buttons.length > 0) {
925
+ const first = buttons.find(b => !b.disabled);
926
+ if (first) {
927
+ await humanDelay();
928
+ try { await first.click(); } catch {}
929
+ }
930
+ }
931
+ return 'alert handled';
932
+ }
933
+
934
+ // ── Run Single Command ──────────────────────────────────────
210
935
  async runCommand(cmdName, prefix) {
211
- const cmdString = `${prefix} ${cmdName}`;
212
- this.log('cmd', `${c.white}${cmdString}${c.reset}`);
936
+ let cmdString;
937
+ const betAmount = this.account.bet_amount || 1000;
938
+
939
+ // Build the command string
940
+ switch (cmdName) {
941
+ case 'dep max': cmdString = `${prefix} dep max`; break;
942
+ case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
943
+ case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
944
+ case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
945
+ case 'slots': cmdString = `${prefix} slots ${betAmount}`; break;
946
+ case 'snakeeyes': cmdString = `${prefix} snakeeyes ${betAmount}`; break;
947
+ case 'work shift': cmdString = `${prefix} work shift`; break;
948
+ default: cmdString = `${prefix} ${cmdName}`;
949
+ }
950
+
951
+ this.log('cmd', `${c.white}${c.bold}${cmdString}${c.reset}`);
952
+ this.stats.commands++;
213
953
 
214
954
  try {
215
955
  await this.channel.send(cmdString);
@@ -217,106 +957,272 @@ class AccountWorker {
217
957
 
218
958
  if (!response) {
219
959
  this.log('warn', `No response for ${cmdString}`);
220
- await sendLog(cmdString, 'No response (timeout)', 'timeout');
960
+ this.stats.errors++;
961
+ await sendLog(this.username, cmdString, 'timeout', 'timeout');
221
962
  return;
222
963
  }
223
964
 
224
- let result;
225
- if (cmdName === 'hl') {
226
- result = await this.handleHighLow(response);
227
- } else if (['search', 'crime'].includes(cmdName)) {
228
- result = await this.handleInteraction(response);
229
- } else {
230
- result = response.content?.substring(0, 100) || 'Embed response';
231
- if (response.components && response.components.length > 0) {
232
- result = await this.handleInteraction(response);
965
+ // Check for cooldown messages
966
+ const responseText = getFullText(response).toLowerCase();
967
+ if (responseText.includes('already claimed') ||
968
+ (responseText.includes('wait') && responseText.includes('before')) ||
969
+ responseText.includes('cooldown') || responseText.includes('slow down') ||
970
+ responseText.includes('come back') || responseText.includes('already used') ||
971
+ responseText.includes('try again in')) {
972
+ this.log('warn', `${cmdName} on cooldown`);
973
+ // Try to parse remaining cooldown from text
974
+ const timeMatch = responseText.match(/(\d+)\s*(second|minute|hour|day)/i);
975
+ if (timeMatch) {
976
+ let secs = parseInt(timeMatch[1]);
977
+ const unit = timeMatch[2].toLowerCase();
978
+ if (unit.startsWith('minute')) secs *= 60;
979
+ if (unit.startsWith('hour')) secs *= 3600;
980
+ if (unit.startsWith('day')) secs *= 86400;
981
+ await this.setCooldown(cmdName, secs + 5);
982
+ this.log('info', `Set cooldown for ${cmdName}: ${secs}s`);
233
983
  }
984
+ await sendLog(this.username, cmdString, 'on cooldown', 'cooldown');
985
+ return;
234
986
  }
235
987
 
236
- this.log('success', `${cmdString} ${c.dim}→${c.reset} ${c.green}${result || 'done'}${c.reset}`);
237
- await sendLog(cmdString, result || 'done', 'success');
988
+ let result;
989
+ switch (cmdName) {
990
+ case 'search': result = await this.handleSearch(response); break;
991
+ case 'crime': result = await this.handleCrime(response); break;
992
+ case 'hl': result = await this.handleHighLow(response); break;
993
+ case 'scratch': result = await this.handleScratch(response); break;
994
+ case 'adventure': result = await this.handleAdventure(response); break;
995
+ case 'blackjack': result = await this.handleBlackjack(response); break;
996
+ case 'trivia': result = await this.handleTrivia(response); break;
997
+ case 'work shift': result = await this.handleWorkShift(response); break;
998
+ case 'coinflip':
999
+ case 'roulette':
1000
+ case 'slots':
1001
+ case 'snakeeyes': result = await this.handleSimpleGamble(response); break;
1002
+ case 'dep max': result = await this.handleDeposit(response); break;
1003
+ case 'alert': result = await this.handleAlert(response); break;
1004
+ default: result = await this.handleGenericCommand(response); break;
1005
+ }
1006
+
1007
+ this.stats.successes++;
1008
+ this.log('success', `${c.dim}${cmdString}${c.reset} → ${result || 'done'}`);
1009
+ await sendLog(this.username, cmdString, result || 'done', 'success');
238
1010
  } catch (err) {
1011
+ this.stats.errors++;
239
1012
  this.log('error', `${cmdString} failed: ${err.message}`);
240
- await sendLog(cmdString, err.message, 'error');
1013
+ await sendLog(this.username, cmdString, err.message, 'error');
241
1014
  }
242
1015
  }
243
1016
 
244
- async grindLoop() {
245
- if (this.running) return;
246
- this.running = true;
1017
+ // ── Redis Cooldown Management ───────────────────────────────
1018
+ async setCooldown(cmdName, durationSeconds) {
1019
+ if (!redis) return;
1020
+ const key = `dkg:cd:${this.account.id}:${cmdName}`;
1021
+ await redis.set(key, '1', 'EX', Math.ceil(durationSeconds));
1022
+ }
247
1023
 
248
- const commandMap = {
249
- cmd_hunt: 'hunt',
250
- cmd_dig: 'dig',
251
- cmd_beg: 'beg',
252
- cmd_search: 'search',
253
- cmd_hl: 'hl',
254
- cmd_crime: 'crime',
255
- cmd_pm: 'pm',
256
- };
1024
+ async isCooldownReady(cmdName) {
1025
+ if (!redis) return true;
1026
+ const key = `dkg:cd:${this.account.id}:${cmdName}`;
1027
+ const val = await redis.get(key);
1028
+ return !val;
1029
+ }
257
1030
 
258
- while (this.running) {
259
- const acc = this.account;
260
- const prefix = acc.use_slash ? '/' : 'pls';
261
- const minCD = acc.cooldown_min || 3;
262
- const maxCD = acc.cooldown_max || 8;
1031
+ printStats() {
1032
+ // Stats are shown in the live dashboard, no-op here
1033
+ }
263
1034
 
264
- const enabledCommands = Object.entries(commandMap)
265
- .filter(([key]) => acc[key] === true || acc[key] === 1)
266
- .map(([, cmd]) => cmd);
1035
+ // ── Main Non-Blocking Grind Scheduler ───────────────────────
1036
+ async tick() {
1037
+ if (!this.running) return;
1038
+ if (this.busy) {
1039
+ this.tickTimeout = setTimeout(() => this.tick(), 1000);
1040
+ return;
1041
+ }
267
1042
 
268
- if (enabledCommands.length === 0) {
269
- this.log('warn', 'No commands enabled. Waiting 30s...');
270
- await new Promise((r) => setTimeout(r, 30000));
271
- continue;
1043
+ const commandMap = [
1044
+ // Grinding commands (high frequency)
1045
+ { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
1046
+ { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 4 },
1047
+ { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 3 },
1048
+ { key: 'cmd_pm', cmd: 'pm', cdKey: 'cd_pm', defaultCd: 20, priority: 3 },
1049
+ { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 2 },
1050
+ { key: 'cmd_hunt', cmd: 'hunt', cdKey: 'cd_hunt', defaultCd: 20, priority: 1 },
1051
+ { key: 'cmd_dig', cmd: 'dig', cdKey: 'cd_dig', defaultCd: 20, priority: 1 },
1052
+ { key: 'cmd_fish', cmd: 'fish', cdKey: 'cd_fish', defaultCd: 20, priority: 1 },
1053
+ { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 2 },
1054
+ { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 1 },
1055
+ // Gambling
1056
+ { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
1057
+ { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
1058
+ { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
1059
+ { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
1060
+ { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
1061
+ // Medium cooldown
1062
+ { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
1063
+ { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 1 },
1064
+ { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 60, priority: 2 },
1065
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 8 },
1066
+ // Economy
1067
+ { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 60, priority: 7 },
1068
+ // Long cooldown / special
1069
+ { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
1070
+ { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
1071
+ { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
1072
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 3600, priority: 8 },
1073
+ { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 3600, priority: 6 },
1074
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 6 },
1075
+ // System
1076
+ { key: 'cmd_alert', cmd: 'alert', cdKey: 'cd_alert', defaultCd: 300, priority: 9 },
1077
+ ];
1078
+
1079
+ const enabledCommands = commandMap.filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
1080
+
1081
+ if (enabledCommands.length === 0) {
1082
+ this.tickTimeout = setTimeout(() => this.tick(), 15000);
1083
+ return;
1084
+ }
1085
+
1086
+ let nextCmdToRun = null;
1087
+ let highestPriority = -1;
1088
+
1089
+ for (const info of enabledCommands) {
1090
+ const ready = await this.isCooldownReady(info.cmd);
1091
+ if (ready && info.priority > highestPriority) {
1092
+ nextCmdToRun = info;
1093
+ highestPriority = info.priority;
272
1094
  }
1095
+ }
1096
+
1097
+ if (nextCmdToRun) {
1098
+ this.busy = true;
1099
+ const cd = (this.account[nextCmdToRun.cdKey] || nextCmdToRun.defaultCd);
1100
+ const jitter = 1 + Math.random() * 3;
1101
+ const totalWait = cd + jitter;
1102
+
1103
+ // Optimistic lock via Redis
1104
+ await this.setCooldown(nextCmdToRun.cmd, totalWait);
1105
+
1106
+ // Global Jitter between commands
1107
+ const now = Date.now();
1108
+ const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1109
+ const globalJitter = 1500 + Math.random() * 1500;
1110
+ if (timeSinceLastCmd < globalJitter) {
1111
+ await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1112
+ }
1113
+
1114
+ const prefix = this.account.use_slash ? '/' : 'pls';
1115
+ this.setStatus(`${c.white}pls ${nextCmdToRun.cmd}${c.reset}`);
1116
+ await this.runCommand(nextCmdToRun.cmd, prefix);
1117
+
1118
+ this.lastCommandRun = Date.now();
1119
+ // Re-apply cooldown based on actual finish time
1120
+ await this.setCooldown(nextCmdToRun.cmd, totalWait);
1121
+
1122
+ this.busy = false;
1123
+ this.cycleCount++;
273
1124
 
274
- for (const cmd of enabledCommands) {
275
- if (!this.running) break;
276
- await this.runCommand(cmd, prefix);
277
- await randomDelay(minCD, maxCD);
1125
+ if (this.cycleCount > 0 && this.cycleCount % 10 === 0) this.printStats();
1126
+ if (this.cycleCount > 0 && this.cycleCount % 20 === 0) {
1127
+ this.busy = true;
1128
+ await this.checkBalance();
1129
+ this.busy = false;
278
1130
  }
279
1131
 
280
- const cycleDelay = 2 + Math.random() * 3;
281
- this.log('info', `${c.dim}Cycle done. Next in ${cycleDelay.toFixed(1)}s${c.reset}`);
282
- await new Promise((r) => setTimeout(r, cycleDelay * 1000));
1132
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1133
+ } else {
1134
+ this.setStatus(`${c.dim}waiting for cooldowns...${c.reset}`);
1135
+ this.tickTimeout = setTimeout(() => this.tick(), 1000);
283
1136
  }
284
1137
  }
285
1138
 
1139
+ async grindLoop() {
1140
+ if (this.running) return;
1141
+ this.running = true;
1142
+ this.busy = false;
1143
+ this.cycleCount = 0;
1144
+ this.lastCommandRun = 0;
1145
+
1146
+ this.configInterval = setInterval(async () => {
1147
+ if (!this.running) return;
1148
+ await this.refreshConfig();
1149
+ if (!this.account.active && !this.busy) {
1150
+ this.log('warn', 'Account deactivated from dashboard. Pausing...');
1151
+ this.busy = true;
1152
+ } else if (this.account.active && this.busy) {
1153
+ this.log('success', 'Account re-activated! Resuming...');
1154
+ this.busy = false;
1155
+ }
1156
+ }, 15000);
1157
+
1158
+ this.tick();
1159
+ }
1160
+
1161
+ async refreshConfig() {
1162
+ try {
1163
+ const res = await fetch(`${API_URL}/api/grinder/status`, {
1164
+ headers: { Authorization: `Bearer ${API_KEY}` },
1165
+ });
1166
+ const data = await res.json();
1167
+ if (data.accounts) {
1168
+ const updated = data.accounts.find((a) => a.id === this.account.id);
1169
+ if (updated) this.account = updated;
1170
+ }
1171
+ } catch { /* silent */ }
1172
+ }
1173
+
286
1174
  async start() {
287
- if (!this.account.discord_token) {
288
- this.log('error', 'No Discord token configured.');
289
- return;
290
- }
291
- if (!this.account.channel_id) {
292
- this.log('error', 'No channel ID configured.');
293
- return;
294
- }
1175
+ if (!this.account.discord_token) { this.log('error', 'No Discord token.'); return; }
1176
+ if (!this.account.channel_id) { this.log('error', 'No channel ID.'); return; }
295
1177
 
296
- this.log('info', 'Connecting to Discord...');
1178
+ this.log('info', 'Connecting...');
297
1179
 
298
1180
  return new Promise((resolve) => {
299
1181
  this.client.on('ready', async () => {
300
- this.log('success', `Logged in as ${c.bold}${this.client.user.tag}${c.reset}`);
301
- this.log('info', `Servers: ${c.white}${this.client.guilds.cache.size}${c.reset}`);
1182
+ this.username = this.client.user.tag || this.username;
1183
+ try {
1184
+ await fetch(`${API_URL}/api/grinder/status`, {
1185
+ method: 'POST',
1186
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1187
+ body: JSON.stringify({ account_id: this.account.id, discord_username: this.username }),
1188
+ });
1189
+ } catch { /* silent */ }
302
1190
 
1191
+ this.log('success', `Logged in! ${c.dim}(${this.client.guilds.cache.size} servers)${c.reset}`);
303
1192
  this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
304
1193
 
305
1194
  if (!this.channel) {
306
- this.log('error', `Cannot find channel ${this.account.channel_id}`);
307
- resolve();
308
- return;
1195
+ this.log('error', `Channel ${this.account.channel_id} not found`);
1196
+ resolve(); return;
309
1197
  }
310
1198
 
311
1199
  this.log('info', `Channel: ${c.white}#${this.channel.name || this.account.channel_id}${c.reset}`);
312
1200
 
313
- const enabled = Object.keys({ cmd_hunt: 1, cmd_dig: 1, cmd_beg: 1, cmd_search: 1, cmd_hl: 1, cmd_crime: 1, cmd_pm: 1 })
314
- .filter((k) => this.account[k] === true || this.account[k] === 1)
315
- .map((k) => k.replace('cmd_', ''));
316
- this.log('info', `Commands: ${c.white}${enabled.join(', ') || 'none'}${c.reset}`);
317
- this.log('info', `Cooldown: ${c.white}${this.account.cooldown_min || 3}-${this.account.cooldown_max || 8}s${c.reset}`);
318
- console.log('');
1201
+ const ALL_CMDS = [
1202
+ { key: 'cmd_hunt', label: 'hunt' }, { key: 'cmd_dig', label: 'dig' },
1203
+ { key: 'cmd_fish', label: 'fish' }, { key: 'cmd_beg', label: 'beg' },
1204
+ { key: 'cmd_search', label: 'search' }, { key: 'cmd_hl', label: 'hl' },
1205
+ { key: 'cmd_crime', label: 'crime' }, { key: 'cmd_pm', label: 'pm' },
1206
+ { key: 'cmd_daily', label: 'daily' }, { key: 'cmd_weekly', label: 'weekly' },
1207
+ { key: 'cmd_monthly', label: 'monthly' }, { key: 'cmd_work', label: 'work' },
1208
+ { key: 'cmd_stream', label: 'stream' }, { key: 'cmd_scratch', label: 'scratch' },
1209
+ { key: 'cmd_adventure', label: 'adventure' }, { key: 'cmd_farm', label: 'farm' },
1210
+ { key: 'cmd_tidy', label: 'tidy' }, { key: 'cmd_blackjack', label: 'bj' },
1211
+ { key: 'cmd_cointoss', label: 'coinflip' }, { key: 'cmd_roulette', label: 'roulette' },
1212
+ { key: 'cmd_slots', label: 'slots' }, { key: 'cmd_snakeeyes', label: 'snakeeyes' },
1213
+ { key: 'cmd_trivia', label: 'trivia' }, { key: 'cmd_use', label: 'use' },
1214
+ { key: 'cmd_deposit', label: 'deposit' }, { key: 'cmd_drops', label: 'drops' },
1215
+ { key: 'cmd_alert', label: 'alert' },
1216
+ ].filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
319
1217
 
1218
+ const cmdStr = ALL_CMDS.map(ci => ci.label).join(', ');
1219
+ this.log('info', `Commands: ${c.white}${cmdStr || 'none'}${c.reset}`);
1220
+ if (this.account.bet_amount) {
1221
+ this.log('info', `Bet amount: ${c.yellow}⏣ ${this.account.bet_amount.toLocaleString()}${c.reset}`);
1222
+ }
1223
+
1224
+ await this.checkBalance();
1225
+ console.log('');
320
1226
  this.grindLoop();
321
1227
  resolve();
322
1228
  });
@@ -327,59 +1233,73 @@ class AccountWorker {
327
1233
 
328
1234
  stop() {
329
1235
  this.running = false;
1236
+ if (this.tickTimeout) clearTimeout(this.tickTimeout);
1237
+ if (this.configInterval) clearInterval(this.configInterval);
330
1238
  try { this.client.destroy(); } catch {}
331
1239
  }
332
1240
  }
333
1241
 
334
- // ── Main Entry ──────────────────────────────────
1242
+ // ══════════════════════════════════════════════════════════════
1243
+ // ═ Main Entry
1244
+ // ══════════════════════════════════════════════════════════════
335
1245
 
336
1246
  async function start(apiKey, apiUrl) {
337
1247
  API_KEY = apiKey;
338
1248
  API_URL = apiUrl;
1249
+ initRedis();
339
1250
 
340
- console.log('');
341
- console.log(` ${c.magenta}${c.bold}╔══════════════════════════════════════════╗${c.reset}`);
342
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
343
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.white}${c.bold}DankGrinder${c.reset} ${c.dim}v2.0${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
344
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.dim}Multi-Account Automation Engine${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
345
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
346
- console.log(` ${c.magenta}${c.bold}╚══════════════════════════════════════════╝${c.reset}`);
1251
+ // Clear screen & show big banner
1252
+ process.stdout.write('\x1b[2J\x1b[H');
1253
+ console.log(BANNER);
1254
+ console.log(` ${c.dim}v4.0${c.reset} ${c.dim}•${c.reset} ${c.white}30 Commands${c.reset} ${c.dim}•${c.reset} ${c.cyan}Redis Cooldowns${c.reset} ${c.dim}•${c.reset} ${c.green}Smart AI${c.reset}`);
347
1255
  console.log('');
348
1256
 
349
1257
  log('info', `API: ${c.dim}${API_URL}${c.reset}`);
350
- log('info', 'Fetching config & accounts...');
1258
+ log('info', `Redis: ${c.dim}${REDIS_URL.replace(/:[^:]+@/, ':***@')}${c.reset}`);
1259
+ log('info', 'Fetching accounts...');
351
1260
 
352
1261
  const data = await fetchConfig();
353
-
354
- if (!data) {
355
- log('error', 'Failed to fetch config. Check your API key and server.');
356
- process.exit(1);
357
- }
1262
+ if (!data) { log('error', 'Failed to fetch config.'); process.exit(1); }
358
1263
 
359
1264
  const { accounts } = data;
360
-
361
1265
  if (!accounts || accounts.length === 0) {
362
- log('error', 'No active accounts found. Add accounts in the dashboard.');
1266
+ log('error', 'No active accounts. Add them in the dashboard.');
363
1267
  process.exit(1);
364
1268
  }
365
1269
 
366
- log('success', `Found ${c.bold}${accounts.length}${c.reset} active account(s)`);
1270
+ log('success', `${c.bold}${accounts.length}${c.reset} active account(s) found`);
367
1271
  console.log('');
368
1272
 
369
- // Spawn a worker per account
370
1273
  for (let i = 0; i < accounts.length; i++) {
371
1274
  const worker = new AccountWorker(accounts[i], i);
372
1275
  workers.push(worker);
373
1276
  await worker.start();
374
1277
  }
375
1278
 
376
- log('info', `${c.bold}All workers running. Press Ctrl+C to stop.${c.reset}`);
377
1279
  console.log('');
1280
+ startTime = Date.now();
1281
+ dashboardStarted = true;
1282
+ process.stdout.write(c.hide); // Hide cursor for clean UI
1283
+
1284
+ // Start the live dashboard refresh loop
1285
+ setInterval(() => renderDashboard(), 1000);
1286
+ renderDashboard(); // Initial render
378
1287
 
379
1288
  process.on('SIGINT', () => {
1289
+ process.stdout.write(c.show); // Show cursor again
1290
+ dashboardStarted = false;
1291
+ console.log('');
1292
+ console.log('');
1293
+ log('warn', 'Shutting down...');
1294
+ console.log('');
1295
+ for (const w of workers) {
1296
+ const rate = w.stats.commands > 0 ? ((w.stats.successes / w.stats.commands) * 100).toFixed(0) : 0;
1297
+ console.log(` ${w.color}${c.bold}${w.username}${c.reset} ${c.green}⏣ ${w.stats.coins.toLocaleString()}${c.reset} earned ${c.dim}│${c.reset} ${w.stats.commands} cmds ${c.dim}│${c.reset} ${rate}% success`);
1298
+ w.stop();
1299
+ }
1300
+ console.log('');
1301
+ console.log(` ${c.yellow}${c.bold}Total: ⏣ ${totalCoins.toLocaleString()}${c.reset} earned in ${formatUptime()}`);
380
1302
  console.log('');
381
- log('warn', 'Shutting down all workers...');
382
- for (const w of workers) w.stop();
383
1303
  setTimeout(() => process.exit(0), 2000);
384
1304
  });
385
1305
  }