dankgrinder 4.1.2 → 4.3.1

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`);
@@ -365,9 +365,12 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
365
365
 
366
366
  // ── Select adventure type from dropdown ─────────────────────
367
367
  if (menus.length > 0) {
368
- // Re-fetch message to get hydrated components (minValues/maxValues)
369
- const freshMsg = await channel.messages.fetch(response.id).catch(() => null);
370
- if (freshMsg) response = freshMsg;
368
+ try {
369
+ if (channel.messages && typeof channel.messages.fetch === 'function') {
370
+ const freshMsg = await channel.messages.fetch(response.id);
371
+ if (freshMsg) response = freshMsg;
372
+ }
373
+ } catch { /* proceed with original */ }
371
374
 
372
375
  // Find the select menu row index
373
376
  let menuRowIdx = -1;
@@ -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
  /**
@@ -99,9 +99,12 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
99
99
  // Handle select menus
100
100
  const menus = getAllSelectMenus(response);
101
101
  if (menus.length > 0) {
102
- // Re-fetch for hydrated components (minValues/maxValues)
103
- const freshMsg = await channel.messages.fetch(response.id).catch(() => null);
104
- if (freshMsg) response = freshMsg;
102
+ try {
103
+ if (channel.messages && typeof channel.messages.fetch === 'function') {
104
+ const freshMsg = await channel.messages.fetch(response.id);
105
+ if (freshMsg) response = freshMsg;
106
+ }
107
+ } catch { /* proceed with original */ }
105
108
  // Find row index of first select menu
106
109
  let menuRowIdx = -1;
107
110
  for (let i = 0; i < (response.components || []).length; i++) {
@@ -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
  /**
@@ -77,9 +79,13 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
77
79
  }
78
80
 
79
81
  // Step 2: Navigate to Coin Shop tab
80
- // Re-fetch message to get hydrated components (minValues/maxValues for selectMenu)
81
- const freshShopMsg = await channel.messages.fetch(response.id).catch(() => null);
82
- if (freshShopMsg) response = freshShopMsg;
82
+ // Try re-fetch for hydrated components; skip if unavailable
83
+ try {
84
+ if (channel.messages && typeof channel.messages.fetch === 'function') {
85
+ const freshShopMsg = await channel.messages.fetch(response.id);
86
+ if (freshShopMsg) response = freshShopMsg;
87
+ }
88
+ } catch { /* proceed with original response */ }
83
89
 
84
90
  const csInfo = findSelectMenuOption(response, 'Coin Shop');
85
91
  if (csInfo) {
@@ -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 };
@@ -13,7 +13,13 @@ const c = {
13
13
  };
14
14
 
15
15
  // ── Logging ──────────────────────────────────────────────────
16
+ // When dashboard is active, suppress direct console output from command handlers.
17
+ // grinder.js sets this to true once the live dashboard starts rendering.
18
+ let _dashboardActive = false;
19
+ function setDashboardActive(val) { _dashboardActive = val; }
20
+
16
21
  function log(label, msg) {
22
+ if (_dashboardActive) return;
17
23
  const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
18
24
  console.log(` ${c.dim}${time}${c.reset} ${label} ${msg}`);
19
25
  }
@@ -250,18 +256,26 @@ function dumpMessage(msg, label) {
250
256
  }
251
257
 
252
258
  // ── Item Detection ───────────────────────────────────────────
259
+ const ITEM_PATTERNS = [
260
+ { item: 'shovel', patterns: ["don't have a shovel", 'need a shovel', 'you need a shovel'] },
261
+ { item: 'fishing pole', patterns: ["don't have a fishing", 'need a fishing', 'you need a fishing pole'] },
262
+ { item: 'hunting rifle', patterns: ["don't have a hunting rifle", 'need a hunting rifle', 'you need a rifle'] },
263
+ { item: 'adventure ticket', patterns: ["don't have a ticket", "don't have an adventure", 'need a ticket', 'need an adventure ticket'] },
264
+ { item: 'keyboard', patterns: ["don't have a keyboard", 'need a keyboard', 'you need a keyboard', 'need following items'] },
265
+ { item: 'mouse', patterns: ["don't have a mouse", 'need a mouse', 'you need a mouse'] },
266
+ ];
267
+
253
268
  function needsItem(text) {
254
269
  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';
270
+ for (const { item, patterns } of ITEM_PATTERNS) {
271
+ for (const p of patterns) {
272
+ if (lower.includes(p)) return item;
273
+ }
274
+ }
275
+ if (lower.includes('need following items') || lower.includes('missing items')) {
276
+ if (lower.includes('keyboard')) return 'keyboard';
277
+ if (lower.includes('mouse')) return 'mouse';
278
+ }
265
279
  return null;
266
280
  }
267
281
 
@@ -269,6 +283,7 @@ module.exports = {
269
283
  DANK_MEMER_ID,
270
284
  c,
271
285
  LOG,
286
+ setDashboardActive,
272
287
  sleep,
273
288
  humanDelay,
274
289
  getFullText,
package/lib/grinder.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
2
  const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
+ const { setDashboardActive } = require('./commands/utils');
4
5
 
5
6
  // ── Terminal Colors & ANSI ───────────────────────────────────
6
7
  const c = {
@@ -108,6 +109,8 @@ function colorBanner() {
108
109
  let dashboardLines = 0;
109
110
  let dashboardStarted = false;
110
111
  let dashboardRendering = false;
112
+ let lastRenderTime = 0;
113
+ let renderPending = false;
111
114
  let totalBalance = 0;
112
115
  let totalCoins = 0;
113
116
  let totalCommands = 0;
@@ -115,6 +118,7 @@ let startTime = Date.now();
115
118
  let shutdownCalled = false;
116
119
  const recentLogs = [];
117
120
  const MAX_LOGS = 4;
121
+ const RENDER_THROTTLE_MS = 250;
118
122
 
119
123
  function formatUptime() {
120
124
  const s = Math.floor((Date.now() - startTime) / 1000);
@@ -132,9 +136,22 @@ function formatCoins(n) {
132
136
  return n.toLocaleString();
133
137
  }
134
138
 
139
+ function scheduleRender() {
140
+ if (renderPending || !dashboardStarted) return;
141
+ const now = Date.now();
142
+ const elapsed = now - lastRenderTime;
143
+ if (elapsed >= RENDER_THROTTLE_MS) {
144
+ renderDashboard();
145
+ } else {
146
+ renderPending = true;
147
+ setTimeout(() => { renderPending = false; renderDashboard(); }, RENDER_THROTTLE_MS - elapsed);
148
+ }
149
+ }
150
+
135
151
  function renderDashboard() {
136
- if (!dashboardStarted || workers.length === 0 || dashboardRendering) return;
152
+ if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
137
153
  dashboardRendering = true;
154
+ lastRenderTime = Date.now();
138
155
 
139
156
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
140
157
  for (const w of workers) {
@@ -144,7 +161,7 @@ function renderDashboard() {
144
161
  }
145
162
 
146
163
  const lines = [];
147
- const tw = Math.min(process.stdout.columns || 80, 78);
164
+ const tw = Math.min(process.stdout.columns || 80, 76);
148
165
  const thinBar = c.dim + '─'.repeat(tw) + c.reset;
149
166
  const thickTop = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
150
167
  const thickBot = rgb(34, 211, 238) + c.bold + '═'.repeat(tw) + c.reset;
@@ -152,30 +169,37 @@ function renderDashboard() {
152
169
  lines.push(thickTop);
153
170
  lines.push(
154
171
  ` ${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}`
172
+ ` ${c.dim}│${c.reset}` +
173
+ ` ${rgb(52, 211, 153)}+${formatCoins(totalCoins)}${c.reset}` +
174
+ ` ${c.dim}│${c.reset}` +
175
+ ` ${c.white}${totalCommands}${c.reset}${c.dim} cmds${c.reset}` +
176
+ ` ${c.dim}│${c.reset}` +
177
+ ` ${rgb(251, 191, 36)}${formatUptime()}${c.reset}` +
178
+ ` ${c.dim}│${c.reset}` +
179
+ ` ${rgb(52, 211, 153)} LIVE${c.reset}`
163
180
  );
164
181
  lines.push(thinBar);
165
182
 
183
+ const nameWidth = Math.min(14, tw > 60 ? 14 : 10);
184
+ const statusWidth = Math.max(12, tw - nameWidth - 30);
185
+
166
186
  for (const wk of workers) {
167
- const last = (wk.lastStatus || 'idle').substring(0, 32);
187
+ const rawStatus = (wk.lastStatus || 'idle').replace(/\x1b\[[0-9;]*m/g, '');
188
+ const last = rawStatus.substring(0, statusWidth);
168
189
  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}`)
190
+ ? (wk.paused ? `${rgb(239, 68, 68)}⏸${c.reset}`
191
+ : wk.dashboardPaused ? `${rgb(251, 191, 36)}⏸${c.reset}`
192
+ : wk.busy ? `${rgb(251, 191, 36)}◉${c.reset}`
193
+ : `${rgb(52, 211, 153)}●${c.reset}`)
170
194
  : `${rgb(239, 68, 68)}○${c.reset}`;
171
- const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, 16).padEnd(16)}${c.reset}`;
195
+ const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameWidth).padEnd(nameWidth)}${c.reset}`;
172
196
  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}`;
197
+ ? `${rgb(251, 191, 36)}⏣${c.reset}${c.white}${formatCoins(wk.stats.balance).padStart(6)}${c.reset}`
198
+ : `${c.dim}⏣ -${c.reset}`;
175
199
  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}`);
200
+ ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins)}${c.reset}`
201
+ : `${c.dim}+0${c.reset}`;
202
+ lines.push(` ${dot} ${name} ${bal} ${earned} ${c.dim}${last}${c.reset}`);
179
203
  }
180
204
 
181
205
  if (recentLogs.length > 0) {
@@ -203,12 +227,15 @@ function log(type, msg, label) {
203
227
  info: '·', success: '✓', error: '✗', warn: '!',
204
228
  cmd: '▸', coin: '$', buy: '♦', bal: '◈', debug: '·',
205
229
  };
206
- const tag = label ? (label.replace(/\x1b\[[0-9;]*m/g, '') + ' ') : '';
230
+ const tagRaw = label ? label.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 12) : '';
207
231
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
232
+ const tw = Math.min(process.stdout.columns || 80, 76);
208
233
  if (dashboardStarted) {
209
- recentLogs.push(`${time} ${icons[type] || '·'} ${tag}${stripped}`.substring(0, 72));
234
+ const maxLen = tw - 4;
235
+ const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
236
+ recentLogs.push(entry.substring(0, maxLen));
210
237
  while (recentLogs.length > MAX_LOGS) recentLogs.shift();
211
- renderDashboard();
238
+ scheduleRender();
212
239
  } else {
213
240
  const colorIcons = {
214
241
  info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
@@ -454,6 +481,8 @@ class AccountWorker {
454
481
  this.cycleCount = 0;
455
482
  this.lastCommandRun = 0;
456
483
  this.paused = false;
484
+ this.dashboardPaused = false;
485
+ this.failStreak = 0;
457
486
  this.globalCooldownUntil = 0;
458
487
  this.commandQueue = null;
459
488
  this.lastHealthCheck = Date.now();
@@ -463,13 +492,13 @@ class AccountWorker {
463
492
 
464
493
  log(type, msg) {
465
494
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
466
- this.lastStatus = stripped.substring(0, 40);
495
+ if (type !== 'debug') this.lastStatus = stripped.substring(0, 28);
467
496
  log(type, msg, this.tag);
468
497
  }
469
498
 
470
499
  setStatus(text) {
471
500
  this.lastStatus = text;
472
- if (dashboardStarted) renderDashboard();
501
+ if (dashboardStarted) scheduleRender();
473
502
  }
474
503
 
475
504
  waitForDankMemer(timeout = 15000) {
@@ -540,15 +569,27 @@ class AccountWorker {
540
569
  });
541
570
  }
542
571
 
543
- // ── Needs Item Detection ────────────────────────────────────
572
+ // ── Needs Item Detection (Trie-like pattern match) ─────────
573
+ static ITEM_PATTERNS = [
574
+ { item: 'shovel', patterns: ["don't have a shovel", 'need a shovel', 'you need a shovel'] },
575
+ { item: 'fishing pole', patterns: ["don't have a fishing", 'need a fishing', 'you need a fishing pole'] },
576
+ { item: 'hunting rifle', patterns: ["don't have a hunting rifle", 'need a hunting rifle', 'you need a rifle'] },
577
+ { item: 'adventure ticket', patterns: ["don't have a ticket", "don't have an adventure", 'need a ticket'] },
578
+ { item: 'keyboard', patterns: ["don't have a keyboard", 'need a keyboard', 'you need a keyboard'] },
579
+ { item: 'mouse', patterns: ["don't have a mouse", 'need a mouse', 'you need a mouse'] },
580
+ ];
581
+
544
582
  needsItem(text) {
545
583
  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';
584
+ for (const { item, patterns } of AccountWorker.ITEM_PATTERNS) {
585
+ for (const p of patterns) {
586
+ if (lower.includes(p)) return item;
587
+ }
588
+ }
589
+ if (lower.includes('need following items') || lower.includes('missing items')) {
590
+ if (lower.includes('keyboard')) return 'keyboard';
591
+ if (lower.includes('mouse')) return 'mouse';
592
+ }
552
593
  return null;
553
594
  }
554
595
 
@@ -767,6 +808,7 @@ class AccountWorker {
767
808
  case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
768
809
  case 'dep max': cmdResult = await commands.runDeposit(cmdOpts); break;
769
810
  case 'alert': cmdResult = await commands.runAlert(cmdOpts); break;
811
+ case 'stream': cmdResult = await commands.runStream(cmdOpts); break;
770
812
  default: cmdResult = await commands.runGeneric({ ...cmdOpts, cmdString, cmdName }); break;
771
813
  }
772
814
 
@@ -775,7 +817,7 @@ class AccountWorker {
775
817
 
776
818
  // Rate limit detection
777
819
  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}`);
820
+ this.log('warn', `Rate limited! 60s global cooldown`);
779
821
  this.globalCooldownUntil = Date.now() + 60000;
780
822
  await this.setCooldown(cmdName, 60);
781
823
  return;
@@ -784,13 +826,34 @@ class AccountWorker {
784
826
  // Captcha/verification detection — pause immediately
785
827
  if (resultLower.includes('captcha') || resultLower.includes('verification') ||
786
828
  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}`);
829
+ this.log('error', `CAPTCHA DETECTED! Worker PAUSED.`);
789
830
  this.paused = true;
790
831
  await sendLog(this.username, cmdString, 'CAPTCHA DETECTED — worker paused', 'error');
791
832
  return;
792
833
  }
793
834
 
835
+ // Premium-only command detection — disable for 24h
836
+ if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
837
+ resultLower.includes('buy the ability to use this command')) {
838
+ this.log('warn', `${cmdName} requires premium — skipping for 24h`);
839
+ await this.setCooldown(cmdName, 86400);
840
+ return;
841
+ }
842
+
843
+ // Already claimed today (daily/weekly) — set long cooldown
844
+ if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
845
+ this.log('info', `${cmdName} already claimed — waiting`);
846
+ const timeMatch = result.match(/<t:(\d+):R>/);
847
+ if (timeMatch) {
848
+ const nextAvail = parseInt(timeMatch[1]) * 1000;
849
+ const waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
850
+ await this.setCooldown(cmdName, waitSec);
851
+ } else {
852
+ await this.setCooldown(cmdName, cmdName === 'daily' ? 86400 : 604800);
853
+ }
854
+ return;
855
+ }
856
+
794
857
  const earned = Math.max(0, cmdResult.coins || 0);
795
858
  const spent = Math.max(0, cmdResult.lost || 0);
796
859
  if (earned > 0) this.stats.coins += earned;
@@ -798,7 +861,7 @@ class AccountWorker {
798
861
 
799
862
  if (cmdResult.holdTightReason) {
800
863
  const reason = cmdResult.holdTightReason;
801
- this.log('warn', `Hold Tight caused by /${reason} — setting 35s cooldown on "${reason}"`);
864
+ this.log('warn', `Hold Tight: /${reason} — 35s cooldown`);
802
865
  const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
803
866
  const mappedCmd = reasonMap[reason] || reason;
804
867
  await this.setCooldown(mappedCmd, 35);
@@ -806,7 +869,7 @@ class AccountWorker {
806
869
  }
807
870
 
808
871
  this.stats.successes++;
809
- const shortResult = result.substring(0, 50).replace(/\n/g, ' ');
872
+ const shortResult = result.substring(0, 30).replace(/\n/g, ' ');
810
873
  this.setStatus(`${cmdName} → ${shortResult}`);
811
874
  await sendLog(this.username, cmdString, result, 'success');
812
875
  reportEarnings(this.account.id, this.username, earned, spent, cmdName);
@@ -869,15 +932,55 @@ class AccountWorker {
869
932
  buildCommandQueue() {
870
933
  const heap = new MinHeap();
871
934
  const now = Date.now();
872
- const enabled = AccountWorker.COMMAND_MAP.filter(
873
- ci => this.account[ci.key] === true || this.account[ci.key] === 1
935
+ let enabled = AccountWorker.COMMAND_MAP.filter(
936
+ ci => Boolean(this.account[ci.key])
874
937
  );
938
+
939
+ if (enabled.length === 0) {
940
+ this.log('warn', `No commands enabled — auto-enabling safe defaults`);
941
+ const safeDefaults = ['cmd_hunt', 'cmd_dig', 'cmd_beg', 'cmd_search', 'cmd_hl', 'cmd_crime', 'cmd_pm'];
942
+ for (const key of safeDefaults) this.account[key] = true;
943
+ enabled = AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key]));
944
+ }
945
+
875
946
  for (const info of enabled) {
876
947
  heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
877
948
  }
878
949
  return heap;
879
950
  }
880
951
 
952
+ // Merge config changes into existing queue without resetting cooldown timings.
953
+ // Uses a HashMap to drain old items, then rebuilds preserving nextRunAt.
954
+ mergeCommandQueue() {
955
+ const enabled = new Set(
956
+ AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key])).map(ci => ci.cmd)
957
+ );
958
+ if (enabled.size === 0) return;
959
+
960
+ const existing = new Map();
961
+ if (this.commandQueue) {
962
+ while (this.commandQueue.size > 0) {
963
+ const item = this.commandQueue.pop();
964
+ existing.set(item.cmd, item);
965
+ }
966
+ }
967
+
968
+ const heap = new MinHeap();
969
+ const now = Date.now();
970
+ for (const info of AccountWorker.COMMAND_MAP) {
971
+ if (!enabled.has(info.cmd)) continue;
972
+ const old = existing.get(info.cmd);
973
+ if (old) {
974
+ old.info = info;
975
+ old.priority = info.priority;
976
+ heap.push(old);
977
+ } else {
978
+ heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
979
+ }
980
+ }
981
+ this.commandQueue = heap;
982
+ }
983
+
881
984
  // ── Health Check: verify Discord client is still connected ──
882
985
  async healthCheck() {
883
986
  if (!this.running || shutdownCalled) return;
@@ -906,7 +1009,12 @@ class AccountWorker {
906
1009
  async tick() {
907
1010
  if (!this.running || shutdownCalled) return;
908
1011
  if (this.paused) {
909
- this.setStatus(`${c.red}${c.bold}PAUSED (captcha/verification)${c.reset}`);
1012
+ this.setStatus('PAUSED (captcha)');
1013
+ this.tickTimeout = setTimeout(() => this.tick(), 5000);
1014
+ return;
1015
+ }
1016
+ if (this.dashboardPaused) {
1017
+ this.setStatus('paused (dashboard)');
910
1018
  this.tickTimeout = setTimeout(() => this.tick(), 5000);
911
1019
  return;
912
1020
  }
@@ -915,46 +1023,46 @@ class AccountWorker {
915
1023
  return;
916
1024
  }
917
1025
 
918
- // Global rate-limit cooldown
919
1026
  const now = Date.now();
1027
+
920
1028
  if (now < this.globalCooldownUntil) {
921
1029
  const waitSec = Math.ceil((this.globalCooldownUntil - now) / 1000);
922
- this.setStatus(`${c.yellow}rate limited (${waitSec}s)${c.reset}`);
1030
+ this.setStatus(`rate limited (${waitSec}s)`);
923
1031
  this.tickTimeout = setTimeout(() => this.tick(), 2000);
924
1032
  return;
925
1033
  }
926
1034
 
927
- // Periodic health check
928
1035
  try { await this.healthCheck(); } catch (e) {
929
1036
  this.log('error', `Health check error: ${e.message}`);
930
1037
  }
931
1038
 
932
- // Rebuild queue if it doesn't exist or is empty (e.g. after config refresh)
933
1039
  if (!this.commandQueue || this.commandQueue.size === 0) {
934
1040
  this.commandQueue = this.buildCommandQueue();
935
1041
  }
936
-
937
- if (this.commandQueue.size === 0) {
1042
+ if (!this.commandQueue || this.commandQueue.size === 0) {
938
1043
  this.tickTimeout = setTimeout(() => this.tick(), 15000);
939
1044
  return;
940
1045
  }
941
1046
 
942
- // Peek the top item — is it ready?
943
1047
  const top = this.commandQueue.peek();
944
1048
  if (top.nextRunAt > now) {
945
1049
  const waitMs = Math.min(top.nextRunAt - now, 2000);
946
- this.setStatus(`${c.dim}waiting for cooldowns...${c.reset}`);
1050
+ this.setStatus('cooldown...');
947
1051
  this.tickTimeout = setTimeout(() => this.tick(), waitMs);
948
1052
  return;
949
1053
  }
950
1054
 
951
- // Pop the command, check Redis cooldown as a secondary gate
952
1055
  const item = this.commandQueue.pop();
1056
+ if (!item) {
1057
+ this.tickTimeout = setTimeout(() => this.tick(), 1000);
1058
+ return;
1059
+ }
1060
+
953
1061
  const ready = await this.isCooldownReady(item.cmd);
954
1062
  if (!ready) {
955
1063
  const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
956
1064
  item.nextRunAt = now + cd * 1000;
957
- this.commandQueue.push(item);
1065
+ if (this.commandQueue) this.commandQueue.push(item);
958
1066
  this.tickTimeout = setTimeout(() => this.tick(), 100);
959
1067
  return;
960
1068
  }
@@ -966,7 +1074,6 @@ class AccountWorker {
966
1074
 
967
1075
  await this.setCooldown(item.cmd, totalWait);
968
1076
 
969
- // Global jitter between commands
970
1077
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
971
1078
  const globalJitter = 1500 + Math.random() * 1500;
972
1079
  if (timeSinceLastCmd < globalJitter) {
@@ -974,15 +1081,28 @@ class AccountWorker {
974
1081
  }
975
1082
 
976
1083
  const prefix = this.account.use_slash ? '/' : 'pls';
977
- this.setStatus(`${c.white}pls ${item.cmd}${c.reset}`);
1084
+ this.setStatus(`pls ${item.cmd}`);
1085
+
1086
+ const beforeCoins = this.stats.coins;
978
1087
  await this.runCommand(item.cmd, prefix);
1088
+ const earned = this.stats.coins - beforeCoins;
1089
+
1090
+ if (earned <= 0 && item.cmd !== 'dep max' && item.cmd !== 'alert') {
1091
+ this.failStreak++;
1092
+ } else {
1093
+ this.failStreak = 0;
1094
+ }
979
1095
 
980
1096
  this.lastCommandRun = Date.now();
981
1097
  await this.setCooldown(item.cmd, totalWait);
982
1098
 
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);
1099
+ // Exponential backoff: if too many consecutive failures, slow down
1100
+ const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
1101
+
1102
+ if (this.commandQueue && this.running && !shutdownCalled) {
1103
+ item.nextRunAt = Date.now() + totalWait * 1000 * backoffMultiplier;
1104
+ this.commandQueue.push(item);
1105
+ }
986
1106
 
987
1107
  this.busy = false;
988
1108
  this.cycleCount++;
@@ -994,7 +1114,9 @@ class AccountWorker {
994
1114
  this.busy = false;
995
1115
  }
996
1116
 
997
- this.tickTimeout = setTimeout(() => this.tick(), 100);
1117
+ if (this.running && !shutdownCalled) {
1118
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1119
+ }
998
1120
  }
999
1121
 
1000
1122
  async grindLoop() {
@@ -1002,6 +1124,8 @@ class AccountWorker {
1002
1124
  this.running = true;
1003
1125
  this.busy = false;
1004
1126
  this.paused = false;
1127
+ this.dashboardPaused = false;
1128
+ this.failStreak = 0;
1005
1129
  this.cycleCount = 0;
1006
1130
  this.lastCommandRun = 0;
1007
1131
  this.commandQueue = this.buildCommandQueue();
@@ -1011,23 +1135,23 @@ class AccountWorker {
1011
1135
  if (!this.running) return;
1012
1136
  await this.refreshConfig();
1013
1137
 
1014
- // Dashboard can un-pause a captcha-paused worker by re-activating it
1015
1138
  if (this.account.active && this.paused) {
1016
- this.log('success', 'Account re-activated from dashboard! Resuming from captcha pause...');
1139
+ this.log('success', 'Captcha cleared! Resuming...');
1017
1140
  this.paused = false;
1018
- this.busy = false;
1141
+ this.dashboardPaused = false;
1019
1142
  }
1020
1143
 
1021
- if (!this.account.active && !this.busy) {
1144
+ if (!this.account.active && !this.dashboardPaused) {
1022
1145
  this.log('warn', 'Account deactivated from dashboard. Pausing...');
1023
- this.busy = true;
1024
- } else if (this.account.active && this.busy && !this.paused) {
1146
+ this.dashboardPaused = true;
1147
+ } else if (this.account.active && this.dashboardPaused) {
1025
1148
  this.log('success', 'Account re-activated! Resuming...');
1026
- this.busy = false;
1149
+ this.dashboardPaused = false;
1027
1150
  }
1028
1151
 
1029
- // Rebuild the command queue on config refresh so newly enabled/disabled commands take effect
1030
- this.commandQueue = this.buildCommandQueue();
1152
+ if (this.commandQueue && !shutdownCalled) {
1153
+ this.mergeCommandQueue();
1154
+ }
1031
1155
  }, 15000);
1032
1156
 
1033
1157
  const safeTickLoop = async () => {
@@ -1095,10 +1219,11 @@ class AccountWorker {
1095
1219
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1096
1220
  { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1097
1221
  { key: 'cmd_alert', l: 'alert' },
1098
- ].filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
1222
+ ].filter((ci) => Boolean(this.account[ci.key]));
1099
1223
 
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}`);
1224
+ const chName = (this.channel.name || '?').substring(0, 12);
1225
+ this.log('success', `#${chName} · ${enabledCmds.length} cmds`);
1226
+ this.setStatus('starting...');
1102
1227
 
1103
1228
  await this.checkBalance();
1104
1229
  this.grindLoop();
@@ -1112,8 +1237,10 @@ class AccountWorker {
1112
1237
  stop() {
1113
1238
  this.running = false;
1114
1239
  this.paused = false;
1115
- if (this.tickTimeout) clearTimeout(this.tickTimeout);
1116
- if (this.configInterval) clearInterval(this.configInterval);
1240
+ this.dashboardPaused = false;
1241
+ this.busy = false;
1242
+ if (this.tickTimeout) { clearTimeout(this.tickTimeout); this.tickTimeout = null; }
1243
+ if (this.configInterval) { clearInterval(this.configInterval); this.configInterval = null; }
1117
1244
  this.commandQueue = null;
1118
1245
  try { this.client.destroy(); } catch {}
1119
1246
  }
@@ -1135,7 +1262,7 @@ async function start(apiKey, apiUrl) {
1135
1262
 
1136
1263
  console.log(colorBanner());
1137
1264
  console.log(
1138
- ` ${rgb(139, 92, 246)}v4.1${c.reset}` +
1265
+ ` ${rgb(139, 92, 246)}v4.3${c.reset}` +
1139
1266
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1140
1267
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1141
1268
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
@@ -1153,7 +1280,7 @@ async function start(apiKey, apiUrl) {
1153
1280
  const data = await fetchConfig();
1154
1281
  if (!data) {
1155
1282
  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}`);
1283
+ log('error', `Check your API key or API URL and try again.`);
1157
1284
  process.exit(1);
1158
1285
  }
1159
1286
 
@@ -1178,17 +1305,32 @@ async function start(apiKey, apiUrl) {
1178
1305
  console.log('');
1179
1306
  startTime = Date.now();
1180
1307
  dashboardStarted = true;
1308
+ setDashboardActive(true);
1181
1309
  process.stdout.write(c.hide);
1182
1310
 
1183
- setInterval(() => renderDashboard(), 1000);
1184
- renderDashboard();
1311
+ setInterval(() => scheduleRender(), 1000);
1312
+ scheduleRender();
1185
1313
 
1314
+ let sigintHandled = false;
1186
1315
  process.on('SIGINT', () => {
1316
+ if (sigintHandled) return;
1317
+ sigintHandled = true;
1187
1318
  shutdownCalled = true;
1188
- process.stdout.write(c.show);
1189
1319
  dashboardStarted = false;
1320
+ setDashboardActive(false);
1321
+ process.stdout.write(c.show);
1322
+
1323
+ // Clear the dashboard area before printing summary
1324
+ if (dashboardLines > 0) {
1325
+ process.stdout.write(c.cursorUp(dashboardLines));
1326
+ for (let i = 0; i < dashboardLines; i++) {
1327
+ process.stdout.write(c.clearLine + '\r\n');
1328
+ }
1329
+ process.stdout.write(c.cursorUp(dashboardLines));
1330
+ }
1331
+
1190
1332
  const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
1191
- console.log('\n');
1333
+ console.log('');
1192
1334
  console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
1193
1335
  console.log(sepBar);
1194
1336
  for (const wk of workers) {
@@ -1202,9 +1344,15 @@ async function start(apiKey, apiUrl) {
1202
1344
  wk.stop();
1203
1345
  }
1204
1346
  console.log(sepBar);
1205
- console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${totalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()}${c.reset}`);
1347
+
1348
+ // Recalculate totals for accurate summary
1349
+ let finalCoins = 0;
1350
+ for (const wk of workers) finalCoins += wk.stats.coins || 0;
1351
+ console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${finalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()}${c.reset}`);
1206
1352
  console.log('');
1207
- setTimeout(() => process.exit(0), 2000);
1353
+
1354
+ if (redis) { try { redis.disconnect(); } catch {} }
1355
+ setTimeout(() => process.exit(0), 1500);
1208
1356
  });
1209
1357
  }
1210
1358
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.1.2",
3
+ "version": "4.3.1",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"