dankgrinder 5.0.2 → 5.0.4

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.
@@ -73,7 +73,7 @@ const LOG = {
73
73
  cmd: (msg) => log(`${c.magenta}▸${c.reset}`, msg),
74
74
  coin: (msg) => log(`${c.yellow}$${c.reset}`, msg),
75
75
  buy: (msg) => log(`${c.blue}♦${c.reset}`, msg),
76
- debug: (msg) => log(`${c.dim}⊙${c.reset}`, msg),
76
+ debug: () => {},
77
77
  };
78
78
 
79
79
  // ── Pre-compiled Regex (avoid recompilation in hot paths) ────
@@ -259,9 +259,9 @@ function flattenComponents(components) {
259
259
  }
260
260
 
261
261
  function getAllButtons(msg) {
262
- if (msg._cv2buttons?.length > 0) return msg._cv2buttons;
262
+ if (msg._cv2buttons?.length > 0) return msg._cv2buttons.filter(b => b.style !== 'LINK' && b.style !== 5);
263
263
  const all = flattenComponents(msg.components);
264
- return all.filter(c => c.type === 2 || c.type === 'BUTTON');
264
+ return all.filter(c => (c.type === 2 || c.type === 'BUTTON') && c.style !== 'LINK' && c.style !== 5);
265
265
  }
266
266
 
267
267
  function getAllSelectMenus(msg) {
@@ -286,6 +286,10 @@ function findSelectMenuOption(msg, label) {
286
286
  // When CV2 fallback is used, waits for the message to update so callers always
287
287
  // get the updated message back (instead of null, which broke multi-round games).
288
288
  async function safeClickButton(msg, button) {
289
+ // Skip LINK buttons (external URLs) — they have style=LINK/5 and no customId
290
+ if (button.style === 'LINK' || button.style === 5 || (!button.customId && !button.custom_id && typeof button.click !== 'function')) {
291
+ return null;
292
+ }
289
293
  if (typeof button.click === 'function') {
290
294
  return button.click();
291
295
  }
@@ -344,66 +348,10 @@ function getHoldTightReason(msg) {
344
348
  return match ? match[1].toLowerCase() : null;
345
349
  }
346
350
 
347
- // ── Debug Logger ─────────────────────────────────────────────
348
- function logMsg(msg, label) {
349
- if (!msg) { LOG.debug(`[${label}] No message`); return; }
350
- // Content
351
- if (msg.content) LOG.debug(`[${label}] content: "${msg.content.substring(0, 200).replace(/\n/g, ' ')}"`);
352
- // Embeds
353
- for (const e of msg.embeds || []) {
354
- if (e.title) LOG.debug(`[${label}] title: "${e.title}"`);
355
- if (e.description) LOG.debug(`[${label}] desc: "${e.description.substring(0, 200).replace(/\n/g, ' ')}"`);
356
- for (const f of e.fields || []) LOG.debug(`[${label}] field: "${f.name}" = "${(f.value || '').substring(0, 150)}"`);
357
- if (e.footer?.text) LOG.debug(`[${label}] footer: "${e.footer.text}"`);
358
- if (e.image?.url) LOG.debug(`[${label}] image: ${e.image.url.substring(0, 80)}`);
359
- }
360
- // Components (buttons, selects, CV2 text)
361
- for (const row of msg.components || []) {
362
- if (!row) continue;
363
- if (row.type === 'TEXT_DISPLAY' && row.content)
364
- LOG.debug(`[${label}] cv2-text: "${row.content.substring(0, 200).replace(/\n/g, ' ')}"`);
365
- if (row.type === 'CONTAINER' || row.type === 'SECTION') {
366
- for (const comp of row.components || []) {
367
- if (comp.type === 'TEXT_DISPLAY' && comp.content)
368
- LOG.debug(`[${label}] cv2-section: "${comp.content.substring(0, 200).replace(/\n/g, ' ')}"`);
369
- if (comp.components) {
370
- for (const sub of comp.components) {
371
- if (sub.type === 'TEXT_DISPLAY' && sub.content)
372
- LOG.debug(`[${label}] cv2-nested: "${sub.content.substring(0, 200).replace(/\n/g, ' ')}"`);
373
- }
374
- }
375
- }
376
- }
377
- for (const comp of row.components || []) {
378
- if (comp.type === 'BUTTON' || comp.type === 2)
379
- LOG.debug(`[${label}] btn: "${comp.label}" emoji=${comp.emoji?.name || '-'} disabled=${comp.disabled} style=${comp.style} id=${(comp.customId || '').substring(0, 40)}`);
380
- if (comp.type === 'STRING_SELECT' || comp.type === 3)
381
- LOG.debug(`[${label}] select: ${comp.customId} [${comp.options?.map(o => `${o.label}${o.default ? '*' : ''}`).join(', ')}]`);
382
- }
383
- }
384
- }
385
-
386
- // ── Dump full message raw (for debugging) ────────────────────
387
- function dumpMessage(msg, label) {
388
- console.log(`\n═══ [${label}] ═══`);
389
- console.log(` author: ${msg.author?.tag} (${msg.author?.id})`);
390
- console.log(` content: "${msg.content || ''}"`);
391
- console.log(` embeds (${msg.embeds?.length || 0}):`);
392
- for (const e of msg.embeds || []) {
393
- console.log(JSON.stringify({
394
- title: e.title, description: e.description,
395
- fields: e.fields?.map(f => ({ name: f.name, value: f.value })),
396
- footer: e.footer?.text, color: e.color,
397
- }, null, 2));
398
- }
399
- console.log(` components (${msg.components?.length || 0}):`);
400
- for (const row of msg.components || []) {
401
- for (const comp of row.components || []) {
402
- console.log(` type=${comp.type} label="${comp.label}" customId="${comp.customId}" disabled=${comp.disabled} style=${comp.style}`);
403
- if (comp.options) console.log(` options: ${JSON.stringify(comp.options.map(o => ({ label: o.label, value: o.value, default: o.default })))}`);
404
- }
405
- }
406
- }
351
+ // logMsg / dumpMessage — disabled in production (no-ops).
352
+ // Enable by setting DEBUG_MSGS=1 env var for troubleshooting.
353
+ function logMsg() {}
354
+ function dumpMessage() {}
407
355
 
408
356
  // ── CV2 (Components V2) Support ──────────────────────────────
409
357
  // Discord's CV2 messages (flag 32768) aren't parsed by the selfbot library.
package/lib/grinder.js CHANGED
@@ -453,17 +453,13 @@ function renderDashboard() {
453
453
 
454
454
  lines.push(bar);
455
455
 
456
- // Use absolute cursor positioning (row 1, col 1) to avoid ghost bar drift
456
+ // Absolute cursor home always draw from row 1
457
457
  process.stdout.write('\x1b[H');
458
- const prevLines = dashboardLines;
459
458
  for (const line of lines) {
460
459
  process.stdout.write(c.clearLine + '\r' + line + '\n');
461
460
  }
462
- // Clear any trailing lines from previous (larger) render
463
- const maxClear = Math.max(prevLines - lines.length, 0) + 3;
464
- for (let i = 0; i < maxClear; i++) {
465
- process.stdout.write(c.clearLine + '\r\n');
466
- }
461
+ // Erase everything below the dashboard (clears ghost bars, trailing lines)
462
+ process.stdout.write('\x1b[J');
467
463
  dashboardLines = lines.length;
468
464
  dashboardRendering = false;
469
465
  }
@@ -565,32 +561,20 @@ async function reportEarnings(accountId, accountName, earned, spent, command) {
565
561
  earningsBatch.push({ account_id: accountId, account_name: accountName, earned, spent, command });
566
562
  }
567
563
 
568
- const feedBatch = new AsyncBatchQueue(async (batch) => {
564
+ // Command feed sends directly (no batching) for real-time dashboard SSE updates.
565
+ // The dashboard live queue depends on instant delivery via eventBus.emit("sse").
566
+ async function reportCommandFeed(accountId, accountName, data) {
569
567
  if (!API_URL) return;
568
+ const normalized = { ...data };
569
+ if (typeof normalized.command === 'string') normalized.command = stripAnsi(normalized.command).replace(/\s+/g, ' ').trim();
570
+ if (typeof normalized.result === 'string') normalized.result = stripAnsi(normalized.result).replace(/\s+/g, ' ').trim();
570
571
  try {
571
- await fetch(`${API_URL}/api/grinder/command-feed-batch`, {
572
+ await fetch(`${API_URL}/api/grinder/command-feed`, {
572
573
  method: 'POST',
573
574
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
574
- body: JSON.stringify({ items: batch }),
575
+ body: JSON.stringify({ account_id: accountId, account_name: accountName, ...normalized }),
575
576
  });
576
- } catch {
577
- for (const item of batch) {
578
- try {
579
- await fetch(`${API_URL}/api/grinder/command-feed`, {
580
- method: 'POST',
581
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
582
- body: JSON.stringify(item),
583
- });
584
- } catch {}
585
- }
586
- }
587
- }, { maxSize: 100, flushMs: 2000 });
588
-
589
- async function reportCommandFeed(accountId, accountName, data) {
590
- const normalized = { ...data };
591
- if (typeof normalized.command === 'string') normalized.command = stripAnsi(normalized.command).replace(/\s+/g, ' ').trim();
592
- if (typeof normalized.result === 'string') normalized.result = stripAnsi(normalized.result).replace(/\s+/g, ' ').trim();
593
- feedBatch.push({ account_id: accountId, account_name: accountName, ...normalized });
577
+ } catch { /* silent — dashboard just won't see this update */ }
594
578
  }
595
579
 
596
580
  function randomDelay(min, max) {
@@ -1886,7 +1870,7 @@ class AccountWorker {
1886
1870
  this.tickTimeout = setTimeout(() => this.tick(), 5000);
1887
1871
  return;
1888
1872
  }
1889
- if (this.busy) {
1873
+ if (this.busy || this._invRunning) {
1890
1874
  this.tickTimeout = setTimeout(() => this.tick(), 2000);
1891
1875
  return;
1892
1876
  }
@@ -2189,9 +2173,9 @@ class AccountWorker {
2189
2173
  // Handle pending actions from dashboard
2190
2174
  if (Array.isArray(data.pendingActions)) {
2191
2175
  for (const action of data.pendingActions) {
2192
- if (action.action === 'check_inventory' && !this.busy) {
2176
+ if (action.action === 'check_inventory' && !this.busy && !this._invRunning) {
2193
2177
  this.log('info', 'Dashboard requested inventory check');
2194
- this.checkInventory().catch(() => {});
2178
+ await this.checkInventory().catch(() => {});
2195
2179
  try {
2196
2180
  await fetch(`${API_URL}/api/grinder/actions`, {
2197
2181
  method: 'DELETE',
@@ -2278,9 +2262,8 @@ class AccountWorker {
2278
2262
  } catch {}
2279
2263
  }
2280
2264
 
2281
- // Let Discord gateway settle before sending first command
2265
+ // Let Discord gateway settle
2282
2266
  await new Promise(r => setTimeout(r, 2500));
2283
- this.grindLoop();
2284
2267
  resolve();
2285
2268
  });
2286
2269
 
@@ -2341,7 +2324,6 @@ async function start(apiKey, apiUrl) {
2341
2324
  API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
2342
2325
  REDIS_URL = process.env.REDIS_URL || '';
2343
2326
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2344
- initRedis();
2345
2327
 
2346
2328
  process.stdout.write('\x1b[2J\x1b[H');
2347
2329
  const tw = Math.min(process.stdout.columns || 80, 78);
@@ -2361,16 +2343,6 @@ async function start(apiKey, apiUrl) {
2361
2343
  );
2362
2344
  console.log(bar);
2363
2345
 
2364
- const checks = [];
2365
- checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2366
- if (REDIS_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}`);
2367
- if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
2368
- if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
2369
- if (CLUSTER_ENABLED) {
2370
- checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
2371
- }
2372
- console.log(` ${checks.join(' ')}`);
2373
-
2374
2346
  log('info', `${c.dim}Fetching accounts...${c.reset}`);
2375
2347
 
2376
2348
  let data = await fetchConfig(4, 2000);
@@ -2381,6 +2353,13 @@ async function start(apiKey, apiUrl) {
2381
2353
  data = await fetchConfig(4, 2000);
2382
2354
  }
2383
2355
 
2356
+ // Pull Redis/Webhook URLs from API config if not in env
2357
+ if (!REDIS_URL && data.redis_url) REDIS_URL = data.redis_url;
2358
+ if (!REDIS_URL && data.redisUrl) REDIS_URL = data.redisUrl;
2359
+ if (!WEBHOOK_URL && data.webhook_url) WEBHOOK_URL = data.webhook_url;
2360
+ if (!WEBHOOK_URL && data.webhookUrl) WEBHOOK_URL = data.webhookUrl;
2361
+ initRedis();
2362
+
2384
2363
  let { accounts } = data;
2385
2364
  if (!accounts || accounts.length === 0) {
2386
2365
  log('error', 'No active accounts. Add them in the dashboard.');
@@ -2397,13 +2376,19 @@ async function start(apiKey, apiUrl) {
2397
2376
  }
2398
2377
  }
2399
2378
 
2379
+ const checks = [];
2380
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2381
+ if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
2382
+ if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
2383
+ if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
2384
+ if (CLUSTER_ENABLED) {
2385
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
2386
+ }
2400
2387
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
2401
- process.stdout.write(c.cursorUp(1));
2402
- process.stdout.write(c.clearLine + '\r');
2403
2388
  console.log(` ${checks.join(' ')}`);
2404
2389
  console.log('');
2405
2390
 
2406
- // All accounts run simultaneously stagger logins in batches to avoid 429s
2391
+ // Phase 1: Login all accounts (staggered to avoid 429s)
2407
2392
  const BATCH_SIZE = 5;
2408
2393
  const BATCH_DELAY_MS = 3000;
2409
2394
  for (let i = 0; i < accounts.length; i++) {
@@ -2413,12 +2398,22 @@ async function start(apiKey, apiUrl) {
2413
2398
  workerMap.set(accounts[i].id, worker);
2414
2399
  await worker.start();
2415
2400
  if ((i + 1) % BATCH_SIZE === 0 && i + 1 < accounts.length) {
2416
- log('info', `${c.dim}Started ${i + 1}/${accounts.length} accounts, next batch in ${BATCH_DELAY_MS / 1000}s...${c.reset}`);
2401
+ log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next batch in ${BATCH_DELAY_MS / 1000}s...${c.reset}`);
2417
2402
  await new Promise(r => setTimeout(r, BATCH_DELAY_MS));
2418
2403
  hintGC();
2419
2404
  }
2420
2405
  }
2421
2406
 
2407
+ // Phase 2: Run inventory on ALL accounts (must complete before any grinding)
2408
+ log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
2409
+ await Promise.all(workers.map(w => w.checkInventory().catch(() => {})));
2410
+ log('success', `${c.dim}All inventories checked. Starting grind loops...${c.reset}`);
2411
+
2412
+ // Phase 3: Start all grind loops
2413
+ for (const w of workers) {
2414
+ if (!shutdownCalled) w.grindLoop();
2415
+ }
2416
+
2422
2417
  startTime = Date.now();
2423
2418
  dashboardStarted = true;
2424
2419
  setDashboardActive(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.0.2",
3
+ "version": "5.0.4",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"