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.
- package/bin/dankgrinder.js +8 -4
- package/lib/commands/crime.js +23 -6
- package/lib/commands/index.js +2 -0
- package/lib/commands/search.js +24 -9
- package/lib/commands/shop.js +2 -0
- package/lib/commands/stream.js +96 -0
- package/lib/commands/utils.js +18 -10
- package/lib/grinder.js +189 -76
- package/package.json +1 -1
package/bin/dankgrinder.js
CHANGED
|
@@ -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>
|
|
23
|
-
--url <url>
|
|
24
|
-
--
|
|
25
|
-
--
|
|
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`);
|
package/lib/commands/crime.js
CHANGED
|
@@ -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
|
-
|
|
27
|
+
const customSet = new Set(customSafe.map(s => s.toLowerCase()));
|
|
28
|
+
for (const btn of clickable) {
|
|
20
29
|
const label = (btn.label || '').toLowerCase();
|
|
21
|
-
|
|
30
|
+
for (const s of customSet) { if (label.includes(s)) return btn; }
|
|
22
31
|
}
|
|
23
32
|
}
|
|
24
|
-
|
|
33
|
+
|
|
34
|
+
for (const btn of clickable) {
|
|
25
35
|
const label = (btn.label || '').toLowerCase();
|
|
26
|
-
|
|
36
|
+
for (const s of SAFE_CRIME_SET) { if (label.includes(s)) return btn; }
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
|
|
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
|
/**
|
package/lib/commands/index.js
CHANGED
|
@@ -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
|
package/lib/commands/search.js
CHANGED
|
@@ -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
|
-
|
|
26
|
+
const clickable = buttons.filter(b => !b.disabled);
|
|
27
|
+
if (clickable.length === 0) return null;
|
|
28
|
+
|
|
21
29
|
if (customSafe && customSafe.length > 0) {
|
|
22
|
-
|
|
30
|
+
const customSet = new Set(customSafe.map(s => s.toLowerCase()));
|
|
31
|
+
for (const btn of clickable) {
|
|
23
32
|
const label = (btn.label || '').toLowerCase();
|
|
24
|
-
|
|
33
|
+
for (const s of customSet) { if (label.includes(s)) return btn; }
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
|
-
|
|
28
|
-
for (const btn of
|
|
36
|
+
|
|
37
|
+
for (const btn of clickable) {
|
|
29
38
|
const label = (btn.label || '').toLowerCase();
|
|
30
|
-
|
|
39
|
+
for (const s of SAFE_SET) { if (label.includes(s)) return btn; }
|
|
31
40
|
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
/**
|
package/lib/commands/shop.js
CHANGED
|
@@ -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 };
|
package/lib/commands/utils.js
CHANGED
|
@@ -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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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,
|
|
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
|
-
`
|
|
156
|
-
`
|
|
157
|
-
`
|
|
158
|
-
`
|
|
159
|
-
`
|
|
160
|
-
`
|
|
161
|
-
`
|
|
162
|
-
`
|
|
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
|
|
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(
|
|
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,
|
|
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}${
|
|
174
|
-
: `${c.dim}⏣
|
|
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)
|
|
177
|
-
: `${c.dim}
|
|
178
|
-
lines.push(` ${dot} ${name} ${bal}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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',
|
|
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',
|
|
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
|
|
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,
|
|
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
|
-
|
|
873
|
-
ci => this.account[ci.key]
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
984
|
-
|
|
985
|
-
|
|
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.
|
|
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', '
|
|
1106
|
+
this.log('success', 'Captcha cleared! Resuming...');
|
|
1017
1107
|
this.paused = false;
|
|
1018
|
-
this.
|
|
1108
|
+
this.dashboardPaused = false;
|
|
1019
1109
|
}
|
|
1020
1110
|
|
|
1021
|
-
if (!this.account.active && !this.
|
|
1111
|
+
if (!this.account.active && !this.dashboardPaused) {
|
|
1022
1112
|
this.log('warn', 'Account deactivated from dashboard. Pausing...');
|
|
1023
|
-
this.
|
|
1024
|
-
} else if (this.account.active && this.
|
|
1113
|
+
this.dashboardPaused = true;
|
|
1114
|
+
} else if (this.account.active && this.dashboardPaused) {
|
|
1025
1115
|
this.log('success', 'Account re-activated! Resuming...');
|
|
1026
|
-
this.
|
|
1116
|
+
this.dashboardPaused = false;
|
|
1027
1117
|
}
|
|
1028
1118
|
|
|
1029
|
-
|
|
1030
|
-
|
|
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]
|
|
1189
|
+
].filter((ci) => Boolean(this.account[ci.key]));
|
|
1099
1190
|
|
|
1100
|
-
const
|
|
1101
|
-
this.log('success',
|
|
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
|
-
|
|
1116
|
-
|
|
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.
|
|
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', `
|
|
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(() =>
|
|
1184
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
1318
|
+
|
|
1319
|
+
if (redis) { try { redis.disconnect(); } catch {} }
|
|
1320
|
+
setTimeout(() => process.exit(0), 1500);
|
|
1208
1321
|
});
|
|
1209
1322
|
}
|
|
1210
1323
|
|