dankgrinder 4.1.2 → 4.3.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.
@@ -19,10 +19,11 @@ if (args.includes('--help') || args.includes('-h')) {
19
19
  npx dankgrinder --key <API_KEY>
20
20
 
21
21
  ${C.b}Options:${C.r}
22
- --key <key> API key from dashboard (required)
23
- --url <url> Override dashboard URL (default: ${DEFAULT_URL})
24
- --help, -h Show this help
25
- --version, -v Show version
22
+ --key <key> API key from dashboard (required)
23
+ --url <url> Override dashboard URL (default: ${DEFAULT_URL})
24
+ --redis <url> Redis URL for cooldowns & trivia DB
25
+ --help, -h Show this help
26
+ --version, -v Show version
26
27
  `);
27
28
  process.exit(0);
28
29
  }
@@ -34,13 +35,16 @@ if (args.includes('--version') || args.includes('-v')) {
34
35
 
35
36
  let apiKey = process.env.DANKGRINDER_KEY || '';
36
37
  let apiUrl = '';
38
+ let redisUrl = '';
37
39
 
38
40
  for (let i = 0; i < args.length; i++) {
39
41
  if (args[i] === '--key' && args[i + 1]) apiKey = args[i + 1];
40
42
  if (args[i] === '--url' && args[i + 1]) apiUrl = args[i + 1];
43
+ if (args[i] === '--redis' && args[i + 1]) redisUrl = args[i + 1];
41
44
  }
42
45
 
43
46
  apiUrl = apiUrl || process.env.DANKGRINDER_URL || DEFAULT_URL;
47
+ if (redisUrl) process.env.REDIS_URL = redisUrl;
44
48
 
45
49
  if (!apiKey) {
46
50
  console.error(`\n ${C.red}✗ Missing API key.${C.r}\n`);
@@ -12,21 +12,38 @@ const SAFE_CRIME_OPTIONS = [
12
12
  'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
13
13
  'money laundering', 'tax fraud', 'insurance fraud', 'scam',
14
14
  ];
15
+ const SAFE_CRIME_SET = new Set(SAFE_CRIME_OPTIONS);
16
+
17
+ const RISKY_CRIME_OPTIONS = new Set([
18
+ 'murder', 'arson', 'assault', 'kidnap', 'terrorism',
19
+ ]);
15
20
 
16
21
  function pickSafeButton(buttons, customSafe) {
17
22
  if (!buttons || buttons.length === 0) return null;
23
+ const clickable = buttons.filter(b => !b.disabled);
24
+ if (clickable.length === 0) return null;
25
+
18
26
  if (customSafe && customSafe.length > 0) {
19
- for (const btn of buttons) {
27
+ const customSet = new Set(customSafe.map(s => s.toLowerCase()));
28
+ for (const btn of clickable) {
20
29
  const label = (btn.label || '').toLowerCase();
21
- if (customSafe.some(s => label.includes(s.toLowerCase()))) return btn;
30
+ for (const s of customSet) { if (label.includes(s)) return btn; }
22
31
  }
23
32
  }
24
- for (const btn of buttons) {
33
+
34
+ for (const btn of clickable) {
25
35
  const label = (btn.label || '').toLowerCase();
26
- if (SAFE_CRIME_OPTIONS.some(s => label.includes(s))) return btn;
36
+ for (const s of SAFE_CRIME_SET) { if (label.includes(s)) return btn; }
27
37
  }
28
- const clickable = buttons.filter(b => !b.disabled);
29
- return clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
38
+
39
+ const safe = clickable.filter(b => {
40
+ const label = (b.label || '').toLowerCase();
41
+ for (const r of RISKY_CRIME_OPTIONS) { if (label.includes(r)) return false; }
42
+ return true;
43
+ });
44
+
45
+ const pool = safe.length > 0 ? safe : clickable;
46
+ return pool[Math.floor(Math.random() * pool.length)];
30
47
  }
31
48
 
32
49
  /**
@@ -19,6 +19,7 @@ const { runWorkShift } = require('./work');
19
19
  const { runCoinflip, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
20
20
  const { runDeposit } = require('./deposit');
21
21
  const { runGeneric, runAlert } = require('./generic');
22
+ const { runStream } = require('./stream');
22
23
  const { buyItem, ITEM_COSTS } = require('./shop');
23
24
  const { getPlayerLevel, meetsLevelRequirement } = require('./profile');
24
25
 
@@ -45,6 +46,7 @@ module.exports = {
45
46
  runDeposit,
46
47
  runGeneric,
47
48
  runAlert,
49
+ runStream,
48
50
  buyItem,
49
51
 
50
52
  // Profile / Level
@@ -14,24 +14,39 @@ const SAFE_SEARCH_LOCATIONS = [
14
14
  'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
15
15
  'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
16
16
  ];
17
+ const SAFE_SET = new Set(SAFE_SEARCH_LOCATIONS);
18
+
19
+ const DANGEROUS_LOCATIONS = new Set([
20
+ 'police', 'area 51', 'jail', 'hospital', 'sewer',
21
+ 'gun', 'warehouse', 'casino', 'bank', 'airport',
22
+ ]);
17
23
 
18
24
  function pickSafeButton(buttons, customSafe) {
19
25
  if (!buttons || buttons.length === 0) return null;
20
- // Try custom safe list first
26
+ const clickable = buttons.filter(b => !b.disabled);
27
+ if (clickable.length === 0) return null;
28
+
21
29
  if (customSafe && customSafe.length > 0) {
22
- for (const btn of buttons) {
30
+ const customSet = new Set(customSafe.map(s => s.toLowerCase()));
31
+ for (const btn of clickable) {
23
32
  const label = (btn.label || '').toLowerCase();
24
- if (customSafe.some(s => label.includes(s.toLowerCase()))) return btn;
33
+ for (const s of customSet) { if (label.includes(s)) return btn; }
25
34
  }
26
35
  }
27
- // Try built-in safe list
28
- for (const btn of buttons) {
36
+
37
+ for (const btn of clickable) {
29
38
  const label = (btn.label || '').toLowerCase();
30
- if (SAFE_SEARCH_LOCATIONS.some(s => label.includes(s))) return btn;
39
+ for (const s of SAFE_SET) { if (label.includes(s)) return btn; }
31
40
  }
32
- // Fallback: random non-disabled
33
- const clickable = buttons.filter(b => !b.disabled);
34
- return clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
41
+
42
+ const safe = clickable.filter(b => {
43
+ const label = (b.label || '').toLowerCase();
44
+ for (const d of DANGEROUS_LOCATIONS) { if (label.includes(d)) return false; }
45
+ return true;
46
+ });
47
+
48
+ const pool = safe.length > 0 ? safe : clickable;
49
+ return pool[Math.floor(Math.random() * pool.length)];
35
50
  }
36
51
 
37
52
  /**
@@ -16,6 +16,8 @@ const ITEM_COSTS = {
16
16
  'shovel': 25000,
17
17
  'fishing pole': 25000,
18
18
  'adventure ticket': 250000,
19
+ 'keyboard': 10000,
20
+ 'mouse': 10000,
19
21
  };
20
22
 
21
23
  /**
@@ -0,0 +1,96 @@
1
+ const {
2
+ LOG, c, getFullText, parseCoins, getAllButtons,
3
+ safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
4
+ } = require('./utils');
5
+ const { buyItem } = require('./shop');
6
+
7
+ const STREAM_ITEMS = ['keyboard', 'mouse'];
8
+
9
+ async function runStream({ channel, waitForDankMemer, client }) {
10
+ LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
11
+
12
+ await channel.send('pls stream');
13
+ let response = await waitForDankMemer(10000);
14
+
15
+ if (!response) {
16
+ LOG.warn('[stream] No response');
17
+ return { result: 'no response', coins: 0 };
18
+ }
19
+
20
+ if (isHoldTight(response)) {
21
+ const reason = getHoldTightReason(response);
22
+ LOG.warn(`[stream] Hold Tight${reason ? ` (reason: /${reason})` : ''}`);
23
+ await sleep(30000);
24
+ return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
25
+ }
26
+
27
+ logMsg(response, 'stream');
28
+ let text = getFullText(response);
29
+
30
+ const lower = text.toLowerCase();
31
+ if (lower.includes('missing items') || lower.includes('need following items') || lower.includes('need a keyboard') || lower.includes('need a mouse')) {
32
+ const itemsToBuy = [];
33
+ if (lower.includes('keyboard')) itemsToBuy.push('keyboard');
34
+ if (lower.includes('mouse')) itemsToBuy.push('mouse');
35
+
36
+ if (itemsToBuy.length === 0) itemsToBuy.push(...STREAM_ITEMS);
37
+
38
+ LOG.warn(`[stream] Missing items: ${c.bold}${itemsToBuy.join(', ')}${c.reset} — auto-buying...`);
39
+
40
+ for (const item of itemsToBuy) {
41
+ const bought = await buyItem({ channel, waitForDankMemer, client, itemName: item, quantity: 1 });
42
+ if (bought) {
43
+ LOG.success(`[stream] Bought ${c.bold}${item}${c.reset}`);
44
+ } else {
45
+ LOG.error(`[stream] Failed to buy ${item}`);
46
+ return { result: `need ${item} (buy failed)`, coins: 0 };
47
+ }
48
+ await humanDelay(1500, 2500);
49
+ }
50
+
51
+ LOG.info('[stream] Retrying stream after buying items...');
52
+ await sleep(3000);
53
+ await channel.send('pls stream');
54
+ response = await waitForDankMemer(10000);
55
+ if (!response) return { result: 'no response after buy', coins: 0 };
56
+ logMsg(response, 'stream-retry');
57
+ text = getFullText(response);
58
+ }
59
+
60
+ const coins = parseCoins(text);
61
+
62
+ const buttons = getAllButtons(response);
63
+ if (buttons.length > 0) {
64
+ const actionBtn = buttons.find(b => !b.disabled && b.label &&
65
+ !b.label.toLowerCase().includes('end') && !b.label.toLowerCase().includes('stop'));
66
+ const btn = actionBtn || buttons.find(b => !b.disabled);
67
+ if (btn) {
68
+ LOG.info(`[stream] Clicking "${btn.label || '?'}"`);
69
+ await humanDelay();
70
+ try {
71
+ await safeClickButton(response, btn);
72
+ const followUp = await waitForDankMemer(8000);
73
+ if (followUp) {
74
+ logMsg(followUp, 'stream-action');
75
+ const fText = getFullText(followUp);
76
+ const fCoins = parseCoins(fText);
77
+ if (fCoins > 0) {
78
+ LOG.coin(`[stream] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
79
+ return { result: `+⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
80
+ }
81
+ }
82
+ } catch (e) {
83
+ LOG.error(`[stream] Click error: ${e.message}`);
84
+ }
85
+ }
86
+ }
87
+
88
+ if (coins > 0) {
89
+ LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
90
+ return { result: `+⏣ ${coins.toLocaleString()}`, coins };
91
+ }
92
+
93
+ return { result: text.substring(0, 60) || 'streamed', coins: 0, nextCooldownSec: 600 };
94
+ }
95
+
96
+ module.exports = { runStream };
@@ -250,18 +250,26 @@ function dumpMessage(msg, label) {
250
250
  }
251
251
 
252
252
  // ── Item Detection ───────────────────────────────────────────
253
+ const ITEM_PATTERNS = [
254
+ { item: 'shovel', patterns: ["don't have a shovel", 'need a shovel', 'you need a shovel'] },
255
+ { item: 'fishing pole', patterns: ["don't have a fishing", 'need a fishing', 'you need a fishing pole'] },
256
+ { item: 'hunting rifle', patterns: ["don't have a hunting rifle", 'need a hunting rifle', 'you need a rifle'] },
257
+ { item: 'adventure ticket', patterns: ["don't have a ticket", "don't have an adventure", 'need a ticket', 'need an adventure ticket'] },
258
+ { item: 'keyboard', patterns: ["don't have a keyboard", 'need a keyboard', 'you need a keyboard', 'need following items'] },
259
+ { item: 'mouse', patterns: ["don't have a mouse", 'need a mouse', 'you need a mouse'] },
260
+ ];
261
+
253
262
  function needsItem(text) {
254
263
  const lower = text.toLowerCase();
255
- if (lower.includes("don't have a shovel") || (lower.includes('need') && lower.includes('shovel')) || lower.includes('you need a shovel'))
256
- return 'shovel';
257
- if (lower.includes("don't have a fishing") || lower.includes('need a fishing') || lower.includes('you need a fishing pole'))
258
- return 'fishing pole';
259
- if (lower.includes("don't have a hunting rifle") || lower.includes('need a hunting rifle') || lower.includes('you need a rifle'))
260
- return 'hunting rifle';
261
- if (lower.includes("don't have") && lower.includes('ticket'))
262
- return 'adventure ticket';
263
- if (lower.includes('need') && lower.includes('ticket'))
264
- return 'adventure ticket';
264
+ for (const { item, patterns } of ITEM_PATTERNS) {
265
+ for (const p of patterns) {
266
+ if (lower.includes(p)) return item;
267
+ }
268
+ }
269
+ if (lower.includes('need following items') || lower.includes('missing items')) {
270
+ if (lower.includes('keyboard')) return 'keyboard';
271
+ if (lower.includes('mouse')) return 'mouse';
272
+ }
265
273
  return null;
266
274
  }
267
275
 
package/lib/grinder.js CHANGED
@@ -108,6 +108,8 @@ function colorBanner() {
108
108
  let dashboardLines = 0;
109
109
  let dashboardStarted = false;
110
110
  let dashboardRendering = false;
111
+ let lastRenderTime = 0;
112
+ let renderPending = false;
111
113
  let totalBalance = 0;
112
114
  let totalCoins = 0;
113
115
  let totalCommands = 0;
@@ -115,6 +117,7 @@ let startTime = Date.now();
115
117
  let shutdownCalled = false;
116
118
  const recentLogs = [];
117
119
  const MAX_LOGS = 4;
120
+ const RENDER_THROTTLE_MS = 250;
118
121
 
119
122
  function formatUptime() {
120
123
  const s = Math.floor((Date.now() - startTime) / 1000);
@@ -132,9 +135,22 @@ function formatCoins(n) {
132
135
  return n.toLocaleString();
133
136
  }
134
137
 
138
+ function scheduleRender() {
139
+ if (renderPending || !dashboardStarted) return;
140
+ const now = Date.now();
141
+ const elapsed = now - lastRenderTime;
142
+ if (elapsed >= RENDER_THROTTLE_MS) {
143
+ renderDashboard();
144
+ } else {
145
+ renderPending = true;
146
+ setTimeout(() => { renderPending = false; renderDashboard(); }, RENDER_THROTTLE_MS - elapsed);
147
+ }
148
+ }
149
+
135
150
  function renderDashboard() {
136
- if (!dashboardStarted || workers.length === 0 || dashboardRendering) return;
151
+ if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
137
152
  dashboardRendering = true;
153
+ lastRenderTime = Date.now();
138
154
 
139
155
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
140
156
  for (const w of workers) {
@@ -144,7 +160,7 @@ function renderDashboard() {
144
160
  }
145
161
 
146
162
  const lines = [];
147
- const tw = Math.min(process.stdout.columns || 80, 78);
163
+ const tw = Math.min(process.stdout.columns || 80, 76);
148
164
  const thinBar = c.dim + '─'.repeat(tw) + c.reset;
149
165
  const thickTop = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
150
166
  const thickBot = rgb(34, 211, 238) + c.bold + '═'.repeat(tw) + c.reset;
@@ -152,30 +168,37 @@ function renderDashboard() {
152
168
  lines.push(thickTop);
153
169
  lines.push(
154
170
  ` ${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}` +
155
- ` ${c.dim}│${c.reset}` +
156
- ` ${rgb(52, 211, 153)}${c.bold}+${formatCoins(totalCoins)}${c.reset} ${c.dim}earned${c.reset}` +
157
- ` ${c.dim}│${c.reset}` +
158
- ` ${c.white}${c.bold}${totalCommands}${c.reset} ${c.dim}cmds${c.reset}` +
159
- ` ${c.dim}│${c.reset}` +
160
- ` ${rgb(251, 191, 36)}${formatUptime()}${c.reset}` +
161
- ` ${c.dim}│${c.reset}` +
162
- ` ${rgb(52, 211, 153)}●${c.reset} ${c.dim}LIVE${c.reset}`
171
+ ` ${c.dim}│${c.reset}` +
172
+ ` ${rgb(52, 211, 153)}+${formatCoins(totalCoins)}${c.reset}` +
173
+ ` ${c.dim}│${c.reset}` +
174
+ ` ${c.white}${totalCommands}${c.reset}${c.dim} cmds${c.reset}` +
175
+ ` ${c.dim}│${c.reset}` +
176
+ ` ${rgb(251, 191, 36)}${formatUptime()}${c.reset}` +
177
+ ` ${c.dim}│${c.reset}` +
178
+ ` ${rgb(52, 211, 153)} LIVE${c.reset}`
163
179
  );
164
180
  lines.push(thinBar);
165
181
 
182
+ const nameWidth = Math.min(14, tw > 60 ? 14 : 10);
183
+ const statusWidth = Math.max(12, tw - nameWidth - 30);
184
+
166
185
  for (const wk of workers) {
167
- const last = (wk.lastStatus || 'idle').substring(0, 32);
186
+ const rawStatus = (wk.lastStatus || 'idle').replace(/\x1b\[[0-9;]*m/g, '');
187
+ const last = rawStatus.substring(0, statusWidth);
168
188
  const dot = wk.running
169
- ? (wk.paused ? `${rgb(251, 191, 36)}⏸${c.reset}` : wk.busy ? `${rgb(251, 191, 36)}◉${c.reset}` : `${rgb(52, 211, 153)}●${c.reset}`)
189
+ ? (wk.paused ? `${rgb(239, 68, 68)}⏸${c.reset}`
190
+ : wk.dashboardPaused ? `${rgb(251, 191, 36)}⏸${c.reset}`
191
+ : wk.busy ? `${rgb(251, 191, 36)}◉${c.reset}`
192
+ : `${rgb(52, 211, 153)}●${c.reset}`)
170
193
  : `${rgb(239, 68, 68)}○${c.reset}`;
171
- const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, 16).padEnd(16)}${c.reset}`;
194
+ const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameWidth).padEnd(nameWidth)}${c.reset}`;
172
195
  const bal = wk.stats.balance > 0
173
- ? `${rgb(251, 191, 36)}⏣${c.reset}${c.white}${c.bold}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
174
- : `${c.dim}⏣ -${c.reset}`;
196
+ ? `${rgb(251, 191, 36)}⏣${c.reset}${c.white}${formatCoins(wk.stats.balance).padStart(6)}${c.reset}`
197
+ : `${c.dim}⏣ -${c.reset}`;
175
198
  const earned = wk.stats.coins > 0
176
- ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins).padStart(6)}${c.reset}`
177
- : `${c.dim} +0${c.reset}`;
178
- lines.push(` ${dot} ${name} ${bal} ${earned} ${c.dim}${last}${c.reset}`);
199
+ ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins)}${c.reset}`
200
+ : `${c.dim}+0${c.reset}`;
201
+ lines.push(` ${dot} ${name} ${bal} ${earned} ${c.dim}${last}${c.reset}`);
179
202
  }
180
203
 
181
204
  if (recentLogs.length > 0) {
@@ -203,12 +226,15 @@ function log(type, msg, label) {
203
226
  info: '·', success: '✓', error: '✗', warn: '!',
204
227
  cmd: '▸', coin: '$', buy: '♦', bal: '◈', debug: '·',
205
228
  };
206
- const tag = label ? (label.replace(/\x1b\[[0-9;]*m/g, '') + ' ') : '';
229
+ const tagRaw = label ? label.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 12) : '';
207
230
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
231
+ const tw = Math.min(process.stdout.columns || 80, 76);
208
232
  if (dashboardStarted) {
209
- recentLogs.push(`${time} ${icons[type] || '·'} ${tag}${stripped}`.substring(0, 72));
233
+ const maxLen = tw - 4;
234
+ const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
235
+ recentLogs.push(entry.substring(0, maxLen));
210
236
  while (recentLogs.length > MAX_LOGS) recentLogs.shift();
211
- renderDashboard();
237
+ scheduleRender();
212
238
  } else {
213
239
  const colorIcons = {
214
240
  info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
@@ -454,6 +480,8 @@ class AccountWorker {
454
480
  this.cycleCount = 0;
455
481
  this.lastCommandRun = 0;
456
482
  this.paused = false;
483
+ this.dashboardPaused = false;
484
+ this.failStreak = 0;
457
485
  this.globalCooldownUntil = 0;
458
486
  this.commandQueue = null;
459
487
  this.lastHealthCheck = Date.now();
@@ -463,13 +491,13 @@ class AccountWorker {
463
491
 
464
492
  log(type, msg) {
465
493
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
466
- this.lastStatus = stripped.substring(0, 40);
494
+ if (type !== 'debug') this.lastStatus = stripped.substring(0, 28);
467
495
  log(type, msg, this.tag);
468
496
  }
469
497
 
470
498
  setStatus(text) {
471
499
  this.lastStatus = text;
472
- if (dashboardStarted) renderDashboard();
500
+ if (dashboardStarted) scheduleRender();
473
501
  }
474
502
 
475
503
  waitForDankMemer(timeout = 15000) {
@@ -540,15 +568,27 @@ class AccountWorker {
540
568
  });
541
569
  }
542
570
 
543
- // ── Needs Item Detection ────────────────────────────────────
571
+ // ── Needs Item Detection (Trie-like pattern match) ─────────
572
+ static ITEM_PATTERNS = [
573
+ { item: 'shovel', patterns: ["don't have a shovel", 'need a shovel', 'you need a shovel'] },
574
+ { item: 'fishing pole', patterns: ["don't have a fishing", 'need a fishing', 'you need a fishing pole'] },
575
+ { item: 'hunting rifle', patterns: ["don't have a hunting rifle", 'need a hunting rifle', 'you need a rifle'] },
576
+ { item: 'adventure ticket', patterns: ["don't have a ticket", "don't have an adventure", 'need a ticket'] },
577
+ { item: 'keyboard', patterns: ["don't have a keyboard", 'need a keyboard', 'you need a keyboard'] },
578
+ { item: 'mouse', patterns: ["don't have a mouse", 'need a mouse', 'you need a mouse'] },
579
+ ];
580
+
544
581
  needsItem(text) {
545
582
  const lower = text.toLowerCase();
546
- if (lower.includes("don't have a shovel") || (lower.includes('need') && lower.includes('shovel')) || lower.includes('you need a shovel'))
547
- return 'shovel';
548
- if (lower.includes("don't have a fishing") || lower.includes('need a fishing') || lower.includes('you need a fishing pole'))
549
- return 'fishing pole';
550
- if (lower.includes("don't have a hunting rifle") || lower.includes('need a hunting rifle') || lower.includes('you need a rifle'))
551
- return 'hunting rifle';
583
+ for (const { item, patterns } of AccountWorker.ITEM_PATTERNS) {
584
+ for (const p of patterns) {
585
+ if (lower.includes(p)) return item;
586
+ }
587
+ }
588
+ if (lower.includes('need following items') || lower.includes('missing items')) {
589
+ if (lower.includes('keyboard')) return 'keyboard';
590
+ if (lower.includes('mouse')) return 'mouse';
591
+ }
552
592
  return null;
553
593
  }
554
594
 
@@ -767,6 +807,7 @@ class AccountWorker {
767
807
  case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
768
808
  case 'dep max': cmdResult = await commands.runDeposit(cmdOpts); break;
769
809
  case 'alert': cmdResult = await commands.runAlert(cmdOpts); break;
810
+ case 'stream': cmdResult = await commands.runStream(cmdOpts); break;
770
811
  default: cmdResult = await commands.runGeneric({ ...cmdOpts, cmdString, cmdName }); break;
771
812
  }
772
813
 
@@ -775,7 +816,7 @@ class AccountWorker {
775
816
 
776
817
  // Rate limit detection
777
818
  if (resultLower.includes('slow down') || resultLower.includes('rate limit') || resultLower.includes('too fast')) {
778
- this.log('warn', `${c.yellow}⚠ Rate limited!${c.reset} Setting 60s global cooldown for ${this.username}`);
819
+ this.log('warn', `Rate limited! 60s global cooldown`);
779
820
  this.globalCooldownUntil = Date.now() + 60000;
780
821
  await this.setCooldown(cmdName, 60);
781
822
  return;
@@ -784,13 +825,34 @@ class AccountWorker {
784
825
  // Captcha/verification detection — pause immediately
785
826
  if (resultLower.includes('captcha') || resultLower.includes('verification') ||
786
827
  resultLower.includes('are you human') || resultLower.includes("prove you're not a bot")) {
787
- this.log('error', `${c.red}${c.bold}🚨 CAPTCHA/VERIFICATION DETECTED for ${this.username}!${c.reset}`);
788
- this.log('error', `${c.red}URGENT: Worker PAUSED. Re-enable manually from the dashboard.${c.reset}`);
828
+ this.log('error', `CAPTCHA DETECTED! Worker PAUSED.`);
789
829
  this.paused = true;
790
830
  await sendLog(this.username, cmdString, 'CAPTCHA DETECTED — worker paused', 'error');
791
831
  return;
792
832
  }
793
833
 
834
+ // Premium-only command detection — disable for 24h
835
+ if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
836
+ resultLower.includes('buy the ability to use this command')) {
837
+ this.log('warn', `${cmdName} requires premium — skipping for 24h`);
838
+ await this.setCooldown(cmdName, 86400);
839
+ return;
840
+ }
841
+
842
+ // Already claimed today (daily/weekly) — set long cooldown
843
+ if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
844
+ this.log('info', `${cmdName} already claimed — waiting`);
845
+ const timeMatch = result.match(/<t:(\d+):R>/);
846
+ if (timeMatch) {
847
+ const nextAvail = parseInt(timeMatch[1]) * 1000;
848
+ const waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
849
+ await this.setCooldown(cmdName, waitSec);
850
+ } else {
851
+ await this.setCooldown(cmdName, cmdName === 'daily' ? 86400 : 604800);
852
+ }
853
+ return;
854
+ }
855
+
794
856
  const earned = Math.max(0, cmdResult.coins || 0);
795
857
  const spent = Math.max(0, cmdResult.lost || 0);
796
858
  if (earned > 0) this.stats.coins += earned;
@@ -798,7 +860,7 @@ class AccountWorker {
798
860
 
799
861
  if (cmdResult.holdTightReason) {
800
862
  const reason = cmdResult.holdTightReason;
801
- this.log('warn', `Hold Tight caused by /${reason} — setting 35s cooldown on "${reason}"`);
863
+ this.log('warn', `Hold Tight: /${reason} — 35s cooldown`);
802
864
  const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
803
865
  const mappedCmd = reasonMap[reason] || reason;
804
866
  await this.setCooldown(mappedCmd, 35);
@@ -806,7 +868,7 @@ class AccountWorker {
806
868
  }
807
869
 
808
870
  this.stats.successes++;
809
- const shortResult = result.substring(0, 50).replace(/\n/g, ' ');
871
+ const shortResult = result.substring(0, 30).replace(/\n/g, ' ');
810
872
  this.setStatus(`${cmdName} → ${shortResult}`);
811
873
  await sendLog(this.username, cmdString, result, 'success');
812
874
  reportEarnings(this.account.id, this.username, earned, spent, cmdName);
@@ -869,9 +931,17 @@ class AccountWorker {
869
931
  buildCommandQueue() {
870
932
  const heap = new MinHeap();
871
933
  const now = Date.now();
872
- const enabled = AccountWorker.COMMAND_MAP.filter(
873
- ci => this.account[ci.key] === true || this.account[ci.key] === 1
934
+ let enabled = AccountWorker.COMMAND_MAP.filter(
935
+ ci => Boolean(this.account[ci.key])
874
936
  );
937
+
938
+ if (enabled.length === 0) {
939
+ this.log('warn', `No commands enabled — auto-enabling safe defaults`);
940
+ const safeDefaults = ['cmd_hunt', 'cmd_dig', 'cmd_beg', 'cmd_search', 'cmd_hl', 'cmd_crime', 'cmd_pm'];
941
+ for (const key of safeDefaults) this.account[key] = true;
942
+ enabled = AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key]));
943
+ }
944
+
875
945
  for (const info of enabled) {
876
946
  heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
877
947
  }
@@ -906,7 +976,12 @@ class AccountWorker {
906
976
  async tick() {
907
977
  if (!this.running || shutdownCalled) return;
908
978
  if (this.paused) {
909
- this.setStatus(`${c.red}${c.bold}PAUSED (captcha/verification)${c.reset}`);
979
+ this.setStatus('PAUSED (captcha)');
980
+ this.tickTimeout = setTimeout(() => this.tick(), 5000);
981
+ return;
982
+ }
983
+ if (this.dashboardPaused) {
984
+ this.setStatus('paused (dashboard)');
910
985
  this.tickTimeout = setTimeout(() => this.tick(), 5000);
911
986
  return;
912
987
  }
@@ -915,46 +990,46 @@ class AccountWorker {
915
990
  return;
916
991
  }
917
992
 
918
- // Global rate-limit cooldown
919
993
  const now = Date.now();
994
+
920
995
  if (now < this.globalCooldownUntil) {
921
996
  const waitSec = Math.ceil((this.globalCooldownUntil - now) / 1000);
922
- this.setStatus(`${c.yellow}rate limited (${waitSec}s)${c.reset}`);
997
+ this.setStatus(`rate limited (${waitSec}s)`);
923
998
  this.tickTimeout = setTimeout(() => this.tick(), 2000);
924
999
  return;
925
1000
  }
926
1001
 
927
- // Periodic health check
928
1002
  try { await this.healthCheck(); } catch (e) {
929
1003
  this.log('error', `Health check error: ${e.message}`);
930
1004
  }
931
1005
 
932
- // Rebuild queue if it doesn't exist or is empty (e.g. after config refresh)
933
1006
  if (!this.commandQueue || this.commandQueue.size === 0) {
934
1007
  this.commandQueue = this.buildCommandQueue();
935
1008
  }
936
-
937
- if (this.commandQueue.size === 0) {
1009
+ if (!this.commandQueue || this.commandQueue.size === 0) {
938
1010
  this.tickTimeout = setTimeout(() => this.tick(), 15000);
939
1011
  return;
940
1012
  }
941
1013
 
942
- // Peek the top item — is it ready?
943
1014
  const top = this.commandQueue.peek();
944
1015
  if (top.nextRunAt > now) {
945
1016
  const waitMs = Math.min(top.nextRunAt - now, 2000);
946
- this.setStatus(`${c.dim}waiting for cooldowns...${c.reset}`);
1017
+ this.setStatus('cooldown...');
947
1018
  this.tickTimeout = setTimeout(() => this.tick(), waitMs);
948
1019
  return;
949
1020
  }
950
1021
 
951
- // Pop the command, check Redis cooldown as a secondary gate
952
1022
  const item = this.commandQueue.pop();
1023
+ if (!item) {
1024
+ this.tickTimeout = setTimeout(() => this.tick(), 1000);
1025
+ return;
1026
+ }
1027
+
953
1028
  const ready = await this.isCooldownReady(item.cmd);
954
1029
  if (!ready) {
955
1030
  const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
956
1031
  item.nextRunAt = now + cd * 1000;
957
- this.commandQueue.push(item);
1032
+ if (this.commandQueue) this.commandQueue.push(item);
958
1033
  this.tickTimeout = setTimeout(() => this.tick(), 100);
959
1034
  return;
960
1035
  }
@@ -966,7 +1041,6 @@ class AccountWorker {
966
1041
 
967
1042
  await this.setCooldown(item.cmd, totalWait);
968
1043
 
969
- // Global jitter between commands
970
1044
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
971
1045
  const globalJitter = 1500 + Math.random() * 1500;
972
1046
  if (timeSinceLastCmd < globalJitter) {
@@ -974,15 +1048,28 @@ class AccountWorker {
974
1048
  }
975
1049
 
976
1050
  const prefix = this.account.use_slash ? '/' : 'pls';
977
- this.setStatus(`${c.white}pls ${item.cmd}${c.reset}`);
1051
+ this.setStatus(`pls ${item.cmd}`);
1052
+
1053
+ const beforeCoins = this.stats.coins;
978
1054
  await this.runCommand(item.cmd, prefix);
1055
+ const earned = this.stats.coins - beforeCoins;
1056
+
1057
+ if (earned <= 0 && item.cmd !== 'dep max' && item.cmd !== 'alert') {
1058
+ this.failStreak++;
1059
+ } else {
1060
+ this.failStreak = 0;
1061
+ }
979
1062
 
980
1063
  this.lastCommandRun = Date.now();
981
1064
  await this.setCooldown(item.cmd, totalWait);
982
1065
 
983
- // Push the command back into the heap with its next available time
984
- item.nextRunAt = Date.now() + totalWait * 1000;
985
- this.commandQueue.push(item);
1066
+ // Exponential backoff: if too many consecutive failures, slow down
1067
+ const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
1068
+
1069
+ if (this.commandQueue && this.running && !shutdownCalled) {
1070
+ item.nextRunAt = Date.now() + totalWait * 1000 * backoffMultiplier;
1071
+ this.commandQueue.push(item);
1072
+ }
986
1073
 
987
1074
  this.busy = false;
988
1075
  this.cycleCount++;
@@ -994,7 +1081,9 @@ class AccountWorker {
994
1081
  this.busy = false;
995
1082
  }
996
1083
 
997
- this.tickTimeout = setTimeout(() => this.tick(), 100);
1084
+ if (this.running && !shutdownCalled) {
1085
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1086
+ }
998
1087
  }
999
1088
 
1000
1089
  async grindLoop() {
@@ -1002,6 +1091,8 @@ class AccountWorker {
1002
1091
  this.running = true;
1003
1092
  this.busy = false;
1004
1093
  this.paused = false;
1094
+ this.dashboardPaused = false;
1095
+ this.failStreak = 0;
1005
1096
  this.cycleCount = 0;
1006
1097
  this.lastCommandRun = 0;
1007
1098
  this.commandQueue = this.buildCommandQueue();
@@ -1011,23 +1102,23 @@ class AccountWorker {
1011
1102
  if (!this.running) return;
1012
1103
  await this.refreshConfig();
1013
1104
 
1014
- // Dashboard can un-pause a captcha-paused worker by re-activating it
1015
1105
  if (this.account.active && this.paused) {
1016
- this.log('success', 'Account re-activated from dashboard! Resuming from captcha pause...');
1106
+ this.log('success', 'Captcha cleared! Resuming...');
1017
1107
  this.paused = false;
1018
- this.busy = false;
1108
+ this.dashboardPaused = false;
1019
1109
  }
1020
1110
 
1021
- if (!this.account.active && !this.busy) {
1111
+ if (!this.account.active && !this.dashboardPaused) {
1022
1112
  this.log('warn', 'Account deactivated from dashboard. Pausing...');
1023
- this.busy = true;
1024
- } else if (this.account.active && this.busy && !this.paused) {
1113
+ this.dashboardPaused = true;
1114
+ } else if (this.account.active && this.dashboardPaused) {
1025
1115
  this.log('success', 'Account re-activated! Resuming...');
1026
- this.busy = false;
1116
+ this.dashboardPaused = false;
1027
1117
  }
1028
1118
 
1029
- // Rebuild the command queue on config refresh so newly enabled/disabled commands take effect
1030
- this.commandQueue = this.buildCommandQueue();
1119
+ if (this.commandQueue && !shutdownCalled) {
1120
+ this.commandQueue = this.buildCommandQueue();
1121
+ }
1031
1122
  }, 15000);
1032
1123
 
1033
1124
  const safeTickLoop = async () => {
@@ -1095,10 +1186,11 @@ class AccountWorker {
1095
1186
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1096
1187
  { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1097
1188
  { key: 'cmd_alert', l: 'alert' },
1098
- ].filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
1189
+ ].filter((ci) => Boolean(this.account[ci.key]));
1099
1190
 
1100
- const cmdList = enabledCmds.map(ci => ci.l).join(' ');
1101
- this.log('success', `${this.tag} ${c.dim}#${(this.channel.name || '?').substring(0, 12)}${c.reset} ${c.dim}[${enabledCmds.length} cmds: ${cmdList}]${c.reset}`);
1191
+ const chName = (this.channel.name || '?').substring(0, 12);
1192
+ this.log('success', `#${chName} · ${enabledCmds.length} cmds`);
1193
+ this.setStatus('starting...');
1102
1194
 
1103
1195
  await this.checkBalance();
1104
1196
  this.grindLoop();
@@ -1112,8 +1204,10 @@ class AccountWorker {
1112
1204
  stop() {
1113
1205
  this.running = false;
1114
1206
  this.paused = false;
1115
- if (this.tickTimeout) clearTimeout(this.tickTimeout);
1116
- if (this.configInterval) clearInterval(this.configInterval);
1207
+ this.dashboardPaused = false;
1208
+ this.busy = false;
1209
+ if (this.tickTimeout) { clearTimeout(this.tickTimeout); this.tickTimeout = null; }
1210
+ if (this.configInterval) { clearInterval(this.configInterval); this.configInterval = null; }
1117
1211
  this.commandQueue = null;
1118
1212
  try { this.client.destroy(); } catch {}
1119
1213
  }
@@ -1135,7 +1229,7 @@ async function start(apiKey, apiUrl) {
1135
1229
 
1136
1230
  console.log(colorBanner());
1137
1231
  console.log(
1138
- ` ${rgb(139, 92, 246)}v4.1${c.reset}` +
1232
+ ` ${rgb(139, 92, 246)}v4.3${c.reset}` +
1139
1233
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1140
1234
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1141
1235
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
@@ -1153,7 +1247,7 @@ async function start(apiKey, apiUrl) {
1153
1247
  const data = await fetchConfig();
1154
1248
  if (!data) {
1155
1249
  log('error', `Cannot connect to API`);
1156
- log('error', `Pass ${c.white}--url${c.reset} with your Railway URL or set ${c.white}DANKGRINDER_URL${c.reset}`);
1250
+ log('error', `Check your API key or API URL and try again.`);
1157
1251
  process.exit(1);
1158
1252
  }
1159
1253
 
@@ -1180,15 +1274,28 @@ async function start(apiKey, apiUrl) {
1180
1274
  dashboardStarted = true;
1181
1275
  process.stdout.write(c.hide);
1182
1276
 
1183
- setInterval(() => renderDashboard(), 1000);
1184
- renderDashboard();
1277
+ setInterval(() => scheduleRender(), 1000);
1278
+ scheduleRender();
1185
1279
 
1280
+ let sigintHandled = false;
1186
1281
  process.on('SIGINT', () => {
1282
+ if (sigintHandled) return;
1283
+ sigintHandled = true;
1187
1284
  shutdownCalled = true;
1188
- process.stdout.write(c.show);
1189
1285
  dashboardStarted = false;
1286
+ process.stdout.write(c.show);
1287
+
1288
+ // Clear the dashboard area before printing summary
1289
+ if (dashboardLines > 0) {
1290
+ process.stdout.write(c.cursorUp(dashboardLines));
1291
+ for (let i = 0; i < dashboardLines; i++) {
1292
+ process.stdout.write(c.clearLine + '\r\n');
1293
+ }
1294
+ process.stdout.write(c.cursorUp(dashboardLines));
1295
+ }
1296
+
1190
1297
  const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
1191
- console.log('\n');
1298
+ console.log('');
1192
1299
  console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
1193
1300
  console.log(sepBar);
1194
1301
  for (const wk of workers) {
@@ -1202,9 +1309,15 @@ async function start(apiKey, apiUrl) {
1202
1309
  wk.stop();
1203
1310
  }
1204
1311
  console.log(sepBar);
1205
- console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${totalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()}${c.reset}`);
1312
+
1313
+ // Recalculate totals for accurate summary
1314
+ let finalCoins = 0;
1315
+ for (const wk of workers) finalCoins += wk.stats.coins || 0;
1316
+ console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${finalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()}${c.reset}`);
1206
1317
  console.log('');
1207
- setTimeout(() => process.exit(0), 2000);
1318
+
1319
+ if (redis) { try { redis.disconnect(); } catch {} }
1320
+ setTimeout(() => process.exit(0), 1500);
1208
1321
  });
1209
1322
  }
1210
1323
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.1.2",
3
+ "version": "4.3.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"