dankgrinder 5.0.1 → 5.0.3

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.
@@ -282,7 +282,9 @@ function findSelectMenuOption(msg, label) {
282
282
  return null;
283
283
  }
284
284
 
285
- // Safe button click — tries library methods first, falls back to raw HTTP for CV2
285
+ // Safe button click — tries library methods first, falls back to raw HTTP for CV2.
286
+ // When CV2 fallback is used, waits for the message to update so callers always
287
+ // get the updated message back (instead of null, which broke multi-round games).
286
288
  async function safeClickButton(msg, button) {
287
289
  if (typeof button.click === 'function') {
288
290
  return button.click();
@@ -295,9 +297,29 @@ async function safeClickButton(msg, button) {
295
297
  // Fall through to CV2 raw interaction fallback.
296
298
  }
297
299
  }
298
- // CV2 fallback: send interaction via raw HTTP
300
+ // CV2 fallback: send interaction via raw HTTP, then wait for the message
301
+ // to update so we can return the updated message to the caller.
299
302
  if (id) {
300
303
  await clickCV2Button(msg, id);
304
+ // Wait for Dank Memer to process the interaction and update the message
305
+ const updatedMsg = await new Promise((resolve) => {
306
+ const timeout = setTimeout(() => {
307
+ msg.client?.removeListener?.('messageUpdate', handler);
308
+ resolve(null);
309
+ }, 8000);
310
+ const handler = (_, newMsg) => {
311
+ if (newMsg.id === msg.id) {
312
+ clearTimeout(timeout);
313
+ msg.client?.removeListener?.('messageUpdate', handler);
314
+ resolve(newMsg);
315
+ }
316
+ };
317
+ msg.client?.on?.('messageUpdate', handler);
318
+ });
319
+ if (updatedMsg) {
320
+ await ensureCV2(updatedMsg);
321
+ return updatedMsg;
322
+ }
301
323
  return null;
302
324
  }
303
325
  throw new Error('No click method available on button');
package/lib/grinder.js CHANGED
@@ -453,19 +453,16 @@ 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
457
+ process.stdout.write('\x1b[H');
456
458
  const prevLines = dashboardLines;
457
- if (prevLines > 0) {
458
- process.stdout.write(c.cursorUp(prevLines));
459
- }
460
459
  for (const line of lines) {
461
460
  process.stdout.write(c.clearLine + '\r' + line + '\n');
462
461
  }
463
- // Clear trailing old lines when dashboard shrinks (prevents ghost bars)
464
- if (lines.length < prevLines) {
465
- for (let i = lines.length; i < prevLines; i++) {
466
- process.stdout.write(c.clearLine + '\r\n');
467
- }
468
- process.stdout.write(c.cursorUp(prevLines - lines.length));
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');
469
466
  }
470
467
  dashboardLines = lines.length;
471
468
  dashboardRendering = false;
@@ -568,32 +565,20 @@ async function reportEarnings(accountId, accountName, earned, spent, command) {
568
565
  earningsBatch.push({ account_id: accountId, account_name: accountName, earned, spent, command });
569
566
  }
570
567
 
571
- const feedBatch = new AsyncBatchQueue(async (batch) => {
568
+ // Command feed sends directly (no batching) for real-time dashboard SSE updates.
569
+ // The dashboard live queue depends on instant delivery via eventBus.emit("sse").
570
+ async function reportCommandFeed(accountId, accountName, data) {
572
571
  if (!API_URL) return;
572
+ const normalized = { ...data };
573
+ if (typeof normalized.command === 'string') normalized.command = stripAnsi(normalized.command).replace(/\s+/g, ' ').trim();
574
+ if (typeof normalized.result === 'string') normalized.result = stripAnsi(normalized.result).replace(/\s+/g, ' ').trim();
573
575
  try {
574
- await fetch(`${API_URL}/api/grinder/command-feed-batch`, {
576
+ await fetch(`${API_URL}/api/grinder/command-feed`, {
575
577
  method: 'POST',
576
578
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
577
- body: JSON.stringify({ items: batch }),
579
+ body: JSON.stringify({ account_id: accountId, account_name: accountName, ...normalized }),
578
580
  });
579
- } catch {
580
- for (const item of batch) {
581
- try {
582
- await fetch(`${API_URL}/api/grinder/command-feed`, {
583
- method: 'POST',
584
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
585
- body: JSON.stringify(item),
586
- });
587
- } catch {}
588
- }
589
- }
590
- }, { maxSize: 100, flushMs: 2000 });
591
-
592
- async function reportCommandFeed(accountId, accountName, data) {
593
- const normalized = { ...data };
594
- if (typeof normalized.command === 'string') normalized.command = stripAnsi(normalized.command).replace(/\s+/g, ' ').trim();
595
- if (typeof normalized.result === 'string') normalized.result = stripAnsi(normalized.result).replace(/\s+/g, ' ').trim();
596
- feedBatch.push({ account_id: accountId, account_name: accountName, ...normalized });
581
+ } catch { /* silent — dashboard just won't see this update */ }
597
582
  }
598
583
 
599
584
  function randomDelay(min, max) {
@@ -1535,11 +1520,13 @@ class AccountWorker {
1535
1520
 
1536
1521
  if (cmdResult.holdTightReason) {
1537
1522
  const reason = cmdResult.holdTightReason;
1538
- this.log('warn', `Hold Tight: /${reason} — 35s cooldown`);
1523
+ const holdSec = 35;
1524
+ this.log('warn', `Hold Tight: /${reason} — ${holdSec}s global cooldown`);
1539
1525
  const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
1540
1526
  const mappedCmd = reasonMap[reason] || reason;
1541
- await this.setCooldown(mappedCmd, 35);
1542
- await this.setCooldown(cmdName, 35);
1527
+ await this.setCooldown(mappedCmd, holdSec);
1528
+ await this.setCooldown(cmdName, holdSec);
1529
+ this.globalCooldownUntil = Math.max(this.globalCooldownUntil, Date.now() + holdSec * 1000);
1543
1530
  }
1544
1531
 
1545
1532
  this.stats.successes++;
@@ -1887,7 +1874,7 @@ class AccountWorker {
1887
1874
  this.tickTimeout = setTimeout(() => this.tick(), 5000);
1888
1875
  return;
1889
1876
  }
1890
- if (this.busy) {
1877
+ if (this.busy || this._invRunning) {
1891
1878
  this.tickTimeout = setTimeout(() => this.tick(), 2000);
1892
1879
  return;
1893
1880
  }
@@ -2025,6 +2012,14 @@ class AccountWorker {
2025
2012
  await this.runCommand(item.cmd, prefix);
2026
2013
  const earned = this.stats.coins - beforeCoins;
2027
2014
 
2015
+ // Grace period for interactive (button-click) commands — Dank Memer
2016
+ // needs time to process the interaction before accepting the next command.
2017
+ // Without this, the next command gets "Hold Tight" errors.
2018
+ const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish']);
2019
+ if (INTERACTIVE_CMDS.has(item.cmd)) {
2020
+ await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
2021
+ }
2022
+
2028
2023
  // EMA: track smoothed earnings per command for adaptive scheduling
2029
2024
  if (earned > 0) {
2030
2025
  this._earningsEMA.update(earned);
@@ -2075,14 +2070,8 @@ class AccountWorker {
2075
2070
  if (this.cycleCount > 0 && this.cycleCount % 10 === 0) this.printStats();
2076
2071
  // Rebuild BloomFilter every 50 cycles to clear expired entries
2077
2072
  if (this.cycleCount > 0 && this.cycleCount % 50 === 0) this._rebuildBloom();
2078
- if (this.cycleCount > 0 && this.cycleCount % 5 === 0) {
2079
- this.busy = true;
2080
- await this.checkBalance();
2081
- this.busy = false;
2082
- }
2083
-
2084
2073
  if (this.running && !shutdownCalled) {
2085
- const nextDelay = this.failStreak > 0 ? 1500 : 500;
2074
+ const nextDelay = this.failStreak > 0 ? 3000 : 1500;
2086
2075
  this.tickTimeout = setTimeout(() => this.tick(), nextDelay);
2087
2076
  }
2088
2077
  }
@@ -2090,7 +2079,6 @@ class AccountWorker {
2090
2079
  async grindLoop() {
2091
2080
  if (this.running) return;
2092
2081
  this.running = true;
2093
- this.busy = false;
2094
2082
  this.paused = false;
2095
2083
  this.dashboardPaused = false;
2096
2084
  this.failStreak = 0;
@@ -2189,9 +2177,9 @@ class AccountWorker {
2189
2177
  // Handle pending actions from dashboard
2190
2178
  if (Array.isArray(data.pendingActions)) {
2191
2179
  for (const action of data.pendingActions) {
2192
- if (action.action === 'check_inventory' && !this.busy) {
2180
+ if (action.action === 'check_inventory' && !this.busy && !this._invRunning) {
2193
2181
  this.log('info', 'Dashboard requested inventory check');
2194
- this.checkInventory().catch(() => {});
2182
+ await this.checkInventory().catch(() => {});
2195
2183
  try {
2196
2184
  await fetch(`${API_URL}/api/grinder/actions`, {
2197
2185
  method: 'DELETE',
@@ -2280,8 +2268,8 @@ class AccountWorker {
2280
2268
 
2281
2269
  // Let Discord gateway settle before sending first command
2282
2270
  await new Promise(r => setTimeout(r, 2500));
2283
- await this.checkBalance();
2284
- this.checkInventory().catch(() => {});
2271
+ // Run initial inventory check (awaited) before grind loop starts
2272
+ await this.checkInventory().catch(() => {});
2285
2273
  this.grindLoop();
2286
2274
  resolve();
2287
2275
  });
@@ -2343,7 +2331,6 @@ async function start(apiKey, apiUrl) {
2343
2331
  API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
2344
2332
  REDIS_URL = process.env.REDIS_URL || '';
2345
2333
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2346
- initRedis();
2347
2334
 
2348
2335
  process.stdout.write('\x1b[2J\x1b[H');
2349
2336
  const tw = Math.min(process.stdout.columns || 80, 78);
@@ -2363,16 +2350,6 @@ async function start(apiKey, apiUrl) {
2363
2350
  );
2364
2351
  console.log(bar);
2365
2352
 
2366
- const checks = [];
2367
- checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2368
- checks.push(REDIS_URL ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${c.dim}○ Redis${c.reset}`);
2369
- checks.push(hasZlib ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}zlib (npm i zlib-sync)${c.reset}`);
2370
- checks.push(WEBHOOK_URL ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}` : `${c.dim}○ Webhook${c.reset}`);
2371
- if (CLUSTER_ENABLED) {
2372
- checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
2373
- }
2374
- console.log(` ${checks.join(' ')}`);
2375
-
2376
2353
  log('info', `${c.dim}Fetching accounts...${c.reset}`);
2377
2354
 
2378
2355
  let data = await fetchConfig(4, 2000);
@@ -2383,6 +2360,13 @@ async function start(apiKey, apiUrl) {
2383
2360
  data = await fetchConfig(4, 2000);
2384
2361
  }
2385
2362
 
2363
+ // Pull Redis/Webhook URLs from API config if not in env
2364
+ if (!REDIS_URL && data.redis_url) REDIS_URL = data.redis_url;
2365
+ if (!REDIS_URL && data.redisUrl) REDIS_URL = data.redisUrl;
2366
+ if (!WEBHOOK_URL && data.webhook_url) WEBHOOK_URL = data.webhook_url;
2367
+ if (!WEBHOOK_URL && data.webhookUrl) WEBHOOK_URL = data.webhookUrl;
2368
+ initRedis();
2369
+
2386
2370
  let { accounts } = data;
2387
2371
  if (!accounts || accounts.length === 0) {
2388
2372
  log('error', 'No active accounts. Add them in the dashboard.');
@@ -2399,9 +2383,15 @@ async function start(apiKey, apiUrl) {
2399
2383
  }
2400
2384
  }
2401
2385
 
2386
+ const checks = [];
2387
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2388
+ 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}`);
2389
+ if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
2390
+ if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
2391
+ if (CLUSTER_ENABLED) {
2392
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
2393
+ }
2402
2394
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
2403
- process.stdout.write(c.cursorUp(1));
2404
- process.stdout.write(c.clearLine + '\r');
2405
2395
  console.log(` ${checks.join(' ')}`);
2406
2396
  console.log('');
2407
2397
 
@@ -2421,11 +2411,13 @@ async function start(apiKey, apiUrl) {
2421
2411
  }
2422
2412
  }
2423
2413
 
2424
- console.log('');
2425
2414
  startTime = Date.now();
2426
2415
  dashboardStarted = true;
2427
2416
  setDashboardActive(true);
2417
+ // Clear entire screen so startup logs don't create ghost bars
2418
+ process.stdout.write('\x1b[2J\x1b[H');
2428
2419
  process.stdout.write(c.hide);
2420
+ dashboardLines = 0;
2429
2421
 
2430
2422
  setInterval(() => scheduleRender(), 1000);
2431
2423
  scheduleRender();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"