dankgrinder 5.24.0 → 5.260.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/lib/antiDetect.js +211 -0
- package/lib/commands/adventure.js +36 -25
- package/lib/commands/dig.js +80 -15
- package/lib/commands/farm.js +14 -3
- package/lib/commands/stream.js +33 -3
- package/lib/commands/utils.js +20 -2
- package/lib/commands/work.js +37 -13
- package/lib/cooldownManager.js +347 -0
- package/lib/grinder.js +159 -55
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -316,37 +316,48 @@ function renderDashboard() {
|
|
|
316
316
|
const tw = Math.min(process.stdout.columns || 80, 78);
|
|
317
317
|
const thinBar = c.dim + '─'.repeat(tw) + c.reset;
|
|
318
318
|
const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
|
|
319
|
+
const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
|
|
320
|
+
const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
|
|
319
321
|
|
|
320
322
|
// Header with dynamic version, command count, and status
|
|
321
|
-
lines.push(
|
|
323
|
+
lines.push(doubleBar);
|
|
322
324
|
const cmdCount = AccountWorker.COMMAND_MAP.length;
|
|
323
325
|
const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
324
326
|
const mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
|
|
327
|
+
|
|
328
|
+
// Animated spinner based on time
|
|
329
|
+
const spinners = ['◐', '◓', '◑', '◒'];
|
|
330
|
+
const spinner = spinners[Math.floor(Date.now() / 250) % 4];
|
|
331
|
+
const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
|
|
332
|
+
|
|
325
333
|
lines.push(
|
|
326
334
|
` ${rgb(139, 92, 246)}${c.bold}DankGrinder${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}` +
|
|
327
335
|
` ${c.dim}·${c.reset} ${c.white}${cmdCount} Cmds${c.reset}` +
|
|
328
336
|
` ${c.dim}·${c.reset} ${mode}` +
|
|
329
|
-
` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
|
|
337
|
+
` ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
|
|
330
338
|
);
|
|
331
339
|
|
|
332
|
-
// Stats row
|
|
333
|
-
const liveIcon = rgb(52, 211, 153) + '
|
|
340
|
+
// Stats row with enhanced visual indicators
|
|
341
|
+
const liveIcon = rgb(52, 211, 153) + '●' + c.reset;
|
|
334
342
|
const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
|
|
335
|
-
const earnStr = `${rgb(52, 211, 153)}
|
|
343
|
+
const earnStr = `${rgb(52, 211, 153)}▲ ${formatCoins(totalCoins)}${c.reset}`;
|
|
336
344
|
// Coins/hour rate
|
|
337
345
|
const elapsedHrs = (Date.now() - startTime) / 3_600_000;
|
|
338
346
|
const coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
|
|
339
347
|
const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
|
|
340
348
|
const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
|
|
341
349
|
const rateStr = successRate >= 95
|
|
342
|
-
? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
|
|
343
|
-
: successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
|
|
344
|
-
: `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
|
|
345
|
-
const upStr = `${rgb(251, 191, 36)}
|
|
346
|
-
// Memory usage (RSS in MB)
|
|
350
|
+
? `${rgb(52, 211, 153)}● ${successRate}%${c.reset}`
|
|
351
|
+
: successRate >= 80 ? `${rgb(251, 191, 36)}● ${successRate}%${c.reset}`
|
|
352
|
+
: `${rgb(239, 68, 68)}● ${successRate}%${c.reset}`;
|
|
353
|
+
const upStr = `${rgb(251, 191, 36)}◷ ${formatUptime()}${c.reset}`;
|
|
354
|
+
// Memory usage (RSS in MB) with bar indicator
|
|
347
355
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
356
|
+
const memPct = Math.min(100, (memMB / 1024) * 100);
|
|
357
|
+
const memBarWidth = Math.floor((memPct / 100) * 10);
|
|
358
|
+
const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
|
|
348
359
|
const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
349
|
-
const memStr = `${memColor}${memMB}MB${c.reset}`;
|
|
360
|
+
const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
|
|
350
361
|
// Commands/minute from SlidingWindowCounter
|
|
351
362
|
const cpmVal = globalCmdRate.getRate().toFixed(1);
|
|
352
363
|
const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
|
|
@@ -394,9 +405,16 @@ function renderDashboard() {
|
|
|
394
405
|
const bal = wk.stats.balance > 0
|
|
395
406
|
? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
|
|
396
407
|
: `${c.dim}⏣ -${c.reset}`;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
408
|
+
|
|
409
|
+
// Mini progress bar for earned coins (visual indicator of activity)
|
|
410
|
+
const earnedNum = wk.stats.coins || 0;
|
|
411
|
+
const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
|
|
412
|
+
const earnedBar = earnedNum > 0
|
|
413
|
+
? `${rgb(52, 211, 153)}${'▰'.repeat(earnedBarWidth)}${c.dim}${'▱'.repeat(5 - earnedBarWidth)}${c.reset}`
|
|
414
|
+
: `${c.dim}▱▱▱▱▱${c.reset}`;
|
|
415
|
+
const earned = earnedNum > 0
|
|
416
|
+
? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
|
|
417
|
+
: `${c.dim}+0${c.reset} ${earnedBar}`;
|
|
400
418
|
|
|
401
419
|
lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
|
|
402
420
|
};
|
|
@@ -451,7 +469,7 @@ function renderDashboard() {
|
|
|
451
469
|
}
|
|
452
470
|
}
|
|
453
471
|
|
|
454
|
-
lines.push(
|
|
472
|
+
lines.push(doubleBarBot);
|
|
455
473
|
|
|
456
474
|
// Absolute cursor home — always draw from row 1
|
|
457
475
|
process.stdout.write('\x1b[H');
|
|
@@ -587,6 +605,51 @@ function safeParseJSON(str, fallback = []) {
|
|
|
587
605
|
try { return JSON.parse(str || '[]'); } catch { return fallback; }
|
|
588
606
|
}
|
|
589
607
|
|
|
608
|
+
// ── Command Result Formatter ──────────────────────────────────
|
|
609
|
+
// Clean command names and format results with color codes
|
|
610
|
+
const CMD_NAMES_CLEAN = {
|
|
611
|
+
bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
|
|
612
|
+
ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
|
|
613
|
+
hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
|
|
614
|
+
tidy: 'Tidy', farm: 'Farm', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly',
|
|
615
|
+
scratch: 'Scratch', adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
|
|
616
|
+
drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
|
|
617
|
+
work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
function formatCommandName(cmd) {
|
|
621
|
+
if (!cmd) return '?';
|
|
622
|
+
const clean = cmd.replace(/^pls\s+/, '').replace(/\s+\d+.*$/, '').trim().toLowerCase();
|
|
623
|
+
return CMD_NAMES_CLEAN[clean] || clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatCommandResult(cmdName, result, earned, spent) {
|
|
627
|
+
const name = formatCommandName(cmdName);
|
|
628
|
+
const net = (earned || 0) - (spent || 0);
|
|
629
|
+
|
|
630
|
+
// Extract clean result text
|
|
631
|
+
let cleanResult = stripAnsi(result || '').replace(/\n/g, ' ').substring(0, 40);
|
|
632
|
+
|
|
633
|
+
// Check for common failure/hold patterns
|
|
634
|
+
if (cleanResult.toLowerCase().includes('hold tight')) return `${name}: ${c.yellow}Hold Tight${c.reset}`;
|
|
635
|
+
if (cleanResult.toLowerCase().includes('cooldown')) return `${name}: ${c.dim}On Cooldown${c.reset}`;
|
|
636
|
+
if (cleanResult.toLowerCase().includes('no response')) return `${name}: ${c.red}No Response${c.reset}`;
|
|
637
|
+
|
|
638
|
+
// Format with earnings/losses
|
|
639
|
+
if (net > 0) {
|
|
640
|
+
return `${name}: ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`;
|
|
641
|
+
} else if (net < 0) {
|
|
642
|
+
return `${name}: ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`;
|
|
643
|
+
} else if (earned === 0 && spent === 0) {
|
|
644
|
+
// No coins changed - show result context
|
|
645
|
+
if (cleanResult.includes('completed') || cleanResult.includes('done')) {
|
|
646
|
+
return `${name}: ${c.dim}Done${c.reset}`;
|
|
647
|
+
}
|
|
648
|
+
return `${name}: ${c.dim}${cleanResult || 'Complete'}${c.reset}`;
|
|
649
|
+
}
|
|
650
|
+
return `${name}: ${c.dim}${cleanResult || 'Complete'}${c.reset}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
590
653
|
// ── Coin Parser — prefers Net:/Winnings: fields, falls back to max ⏣ ──
|
|
591
654
|
function parseCoins(text) {
|
|
592
655
|
if (!text) return 0;
|
|
@@ -764,6 +827,13 @@ class MinHeap {
|
|
|
764
827
|
// ══════════════════════════════════════════════════════════════
|
|
765
828
|
|
|
766
829
|
class AccountWorker {
|
|
830
|
+
static SMART_CD_FLOORS = Object.freeze({
|
|
831
|
+
'farm': 30,
|
|
832
|
+
'adventure': 300,
|
|
833
|
+
'stream': 600,
|
|
834
|
+
'work shift': 1800,
|
|
835
|
+
});
|
|
836
|
+
|
|
767
837
|
constructor(account, idx) {
|
|
768
838
|
this.account = account;
|
|
769
839
|
this.idx = idx;
|
|
@@ -1114,7 +1184,10 @@ class AccountWorker {
|
|
|
1114
1184
|
accountId: this.account.id,
|
|
1115
1185
|
redis,
|
|
1116
1186
|
onPageProgress: ({ page, total }) => {
|
|
1117
|
-
|
|
1187
|
+
// Update same line instead of spamming new lines - uses cursor control
|
|
1188
|
+
const progress = `[inv] ${this.username}: ${page}/${total} pages`;
|
|
1189
|
+
const erase = '\x1b[2K\r'; // Clear line + carriage return
|
|
1190
|
+
process.stdout.write(`${erase}${this.color}${progress}${c.reset}`);
|
|
1118
1191
|
},
|
|
1119
1192
|
});
|
|
1120
1193
|
|
|
@@ -1122,6 +1195,8 @@ class AccountWorker {
|
|
|
1122
1195
|
throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
|
|
1123
1196
|
}
|
|
1124
1197
|
|
|
1198
|
+
// Add newline after inventory progress
|
|
1199
|
+
process.stdout.write('\n');
|
|
1125
1200
|
this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
|
|
1126
1201
|
try {
|
|
1127
1202
|
await fetch(`${API_URL}/api/grinder/inventory`, {
|
|
@@ -1524,7 +1599,8 @@ class AccountWorker {
|
|
|
1524
1599
|
|
|
1525
1600
|
const earned = Math.max(0, cmdResult.coins || 0);
|
|
1526
1601
|
const spent = Math.max(0, cmdResult.lost || 0);
|
|
1527
|
-
|
|
1602
|
+
// Track net earnings (add wins, subtract losses)
|
|
1603
|
+
this.stats.coins += (earned - spent);
|
|
1528
1604
|
if (cmdResult.nextCooldownSec) {
|
|
1529
1605
|
await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
|
|
1530
1606
|
this._lastCooldownOverride = cmdResult.nextCooldownSec;
|
|
@@ -1571,8 +1647,9 @@ class AccountWorker {
|
|
|
1571
1647
|
}
|
|
1572
1648
|
|
|
1573
1649
|
this.stats.successes++;
|
|
1574
|
-
|
|
1575
|
-
|
|
1650
|
+
// Format result with clean command name and colored earnings
|
|
1651
|
+
const formattedResult = formatCommandResult(cmdName, result, earned, spent);
|
|
1652
|
+
this.setStatus(formattedResult);
|
|
1576
1653
|
await sendLog(this.username, cmdName, result, 'success');
|
|
1577
1654
|
reportEarnings(this.account.id, this.username, earned, spent, cmdName);
|
|
1578
1655
|
|
|
@@ -1667,7 +1744,7 @@ class AccountWorker {
|
|
|
1667
1744
|
{ key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
|
|
1668
1745
|
// Fast grinders — 10s CD
|
|
1669
1746
|
{ key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
|
|
1670
|
-
|
|
1747
|
+
{ key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
|
|
1671
1748
|
{ key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
|
|
1672
1749
|
{ key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
|
|
1673
1750
|
// Medium grinders — 20-25s CD
|
|
@@ -1681,10 +1758,10 @@ class AccountWorker {
|
|
|
1681
1758
|
{ key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
|
|
1682
1759
|
{ key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
|
|
1683
1760
|
// Interactive — response-driven CD (handler sets nextCooldownSec)
|
|
1684
|
-
|
|
1685
|
-
|
|
1761
|
+
{ key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
|
|
1762
|
+
{ key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
|
|
1686
1763
|
{ key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 10, priority: 3 },
|
|
1687
|
-
|
|
1764
|
+
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
1688
1765
|
// Time-gated (run ASAP when available)
|
|
1689
1766
|
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
|
|
1690
1767
|
{ key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
|
|
@@ -2029,8 +2106,6 @@ class AccountWorker {
|
|
|
2029
2106
|
const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
|
|
2030
2107
|
const totalWait = cd + jitterBase + microPause;
|
|
2031
2108
|
|
|
2032
|
-
await this.setCooldown(item.cmd, totalWait);
|
|
2033
|
-
|
|
2034
2109
|
const timeSinceLastCmd = now - (this.lastCommandRun || 0);
|
|
2035
2110
|
const gapBase = cd <= 5 ? 1500 : cd <= 20 ? 2000 : 2500;
|
|
2036
2111
|
const jitterGap = patternMod.minDelay + Math.random() * (patternMod.maxDelay - patternMod.minDelay);
|
|
@@ -2059,7 +2134,7 @@ class AccountWorker {
|
|
|
2059
2134
|
// Grace period for interactive (button-click) commands — Dank Memer
|
|
2060
2135
|
// needs time to process the interaction before accepting the next command.
|
|
2061
2136
|
// Without this, the next command gets "Hold Tight" errors.
|
|
2062
|
-
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish']);
|
|
2137
|
+
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
|
|
2063
2138
|
if (INTERACTIVE_CMDS.has(item.cmd)) {
|
|
2064
2139
|
await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
|
|
2065
2140
|
}
|
|
@@ -2078,8 +2153,7 @@ class AccountWorker {
|
|
|
2078
2153
|
this.failStreak = 0;
|
|
2079
2154
|
}
|
|
2080
2155
|
|
|
2081
|
-
|
|
2082
|
-
await this.setCooldown(item.cmd, totalWait);
|
|
2156
|
+
this.lastCommandRun = Date.now();
|
|
2083
2157
|
|
|
2084
2158
|
// Exponential backoff: if too many consecutive failures, slow down
|
|
2085
2159
|
const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
|
|
@@ -2087,12 +2161,25 @@ class AccountWorker {
|
|
|
2087
2161
|
const MIN_FAIL_COOLDOWN = 5;
|
|
2088
2162
|
|
|
2089
2163
|
if (this.commandQueue && this.running && !shutdownCalled) {
|
|
2090
|
-
|
|
2164
|
+
const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
|
|
2165
|
+
let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
|
|
2091
2166
|
this._lastCooldownOverride = null;
|
|
2167
|
+
|
|
2168
|
+
// Smart fallback floors for long/interactive commands when parser misses exact cooldown.
|
|
2169
|
+
if (!hasOverride) {
|
|
2170
|
+
const floor = AccountWorker.SMART_CD_FLOORS[item.cmd];
|
|
2171
|
+
if (Number.isFinite(floor) && floor > 0) {
|
|
2172
|
+
effectiveWait = Math.max(effectiveWait, floor);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2092
2176
|
if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
|
|
2093
2177
|
effectiveWait = MIN_FAIL_COOLDOWN;
|
|
2094
2178
|
}
|
|
2095
|
-
|
|
2179
|
+
|
|
2180
|
+
const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
|
|
2181
|
+
await this.setCooldown(item.cmd, scheduledWaitSec);
|
|
2182
|
+
item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
|
|
2096
2183
|
this.commandQueue.push(item);
|
|
2097
2184
|
}
|
|
2098
2185
|
|
|
@@ -2311,8 +2398,8 @@ class AccountWorker {
|
|
|
2311
2398
|
} catch {}
|
|
2312
2399
|
}
|
|
2313
2400
|
|
|
2314
|
-
// Let Discord gateway settle
|
|
2315
|
-
await new Promise(r => setTimeout(r,
|
|
2401
|
+
// Let Discord gateway settle (reduced for faster startup)
|
|
2402
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2316
2403
|
resolve();
|
|
2317
2404
|
});
|
|
2318
2405
|
|
|
@@ -2437,43 +2524,58 @@ async function start(apiKey, apiUrl) {
|
|
|
2437
2524
|
console.log(` ${checks.join(' ')}`);
|
|
2438
2525
|
console.log('');
|
|
2439
2526
|
|
|
2440
|
-
// Phase 1: Login all accounts (
|
|
2441
|
-
const LOGIN_PROGRESS_EVERY =
|
|
2442
|
-
|
|
2443
|
-
const
|
|
2444
|
-
const
|
|
2445
|
-
const
|
|
2527
|
+
// Phase 1: Login all accounts (optimized for speed)
|
|
2528
|
+
const LOGIN_PROGRESS_EVERY = 10;
|
|
2529
|
+
// Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
|
|
2530
|
+
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
2531
|
+
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
|
|
2532
|
+
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
|
|
2533
|
+
const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 150);
|
|
2446
2534
|
|
|
2447
2535
|
const randomLoginGap = () => {
|
|
2448
2536
|
if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
|
|
2449
2537
|
return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
|
|
2450
2538
|
};
|
|
2451
2539
|
|
|
2452
|
-
|
|
2540
|
+
// Parallel login in batches of 10 to avoid rate limits while being fast
|
|
2541
|
+
const BATCH_SIZE = 10;
|
|
2542
|
+
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2453
2543
|
if (shutdownCalled) break;
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
await
|
|
2458
|
-
|
|
2544
|
+
const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
|
|
2545
|
+
|
|
2546
|
+
// Login batch in parallel
|
|
2547
|
+
await Promise.all(batch.map(async (acc, idx) => {
|
|
2548
|
+
const worker = new AccountWorker(acc, i + idx);
|
|
2549
|
+
workers.push(worker);
|
|
2550
|
+
workerMap.set(acc.id, worker);
|
|
2551
|
+
await worker.start();
|
|
2552
|
+
}));
|
|
2553
|
+
|
|
2554
|
+
// Small gap between batches
|
|
2555
|
+
if (i + BATCH_SIZE < accounts.length) {
|
|
2459
2556
|
const gapMs = randomLoginGap();
|
|
2460
|
-
|
|
2461
|
-
log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next account in ${gapMs}ms...${c.reset}`);
|
|
2462
|
-
}
|
|
2557
|
+
log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
|
|
2463
2558
|
await new Promise(r => setTimeout(r, gapMs));
|
|
2464
2559
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
}
|
|
2560
|
+
|
|
2561
|
+
hintGC();
|
|
2468
2562
|
}
|
|
2469
2563
|
|
|
2470
2564
|
// Phase 2: Run inventory on ALL accounts (must complete before any grinding)
|
|
2471
2565
|
log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
|
|
2472
2566
|
let invDone = 0;
|
|
2473
2567
|
let invFailed = 0;
|
|
2474
|
-
|
|
2568
|
+
|
|
2569
|
+
// Use sequential inventory checks with single-line progress update
|
|
2570
|
+
const totalAccounts = workers.length;
|
|
2571
|
+
for (let i = 0; i < workers.length; i++) {
|
|
2572
|
+
const w = workers[i];
|
|
2475
2573
|
const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
|
|
2476
|
-
|
|
2574
|
+
|
|
2575
|
+
// Update single line instead of spamming new lines
|
|
2576
|
+
const progressLine = `\x1b[2K\r${c.dim}[inv] ${i + 1}/${totalAccounts}: ${label}${c.reset}`;
|
|
2577
|
+
process.stdout.write(progressLine);
|
|
2578
|
+
|
|
2477
2579
|
try {
|
|
2478
2580
|
const invRes = await w.checkInventory({
|
|
2479
2581
|
force: true,
|
|
@@ -2486,9 +2588,11 @@ async function start(apiKey, apiUrl) {
|
|
|
2486
2588
|
} catch {
|
|
2487
2589
|
invFailed++;
|
|
2488
2590
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// Newline after progress
|
|
2594
|
+
process.stdout.write('\n');
|
|
2595
|
+
log('success', `${c.dim}Inventory: ${invDone}/${workers.length} complete${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}${c.reset}`);
|
|
2492
2596
|
|
|
2493
2597
|
if (invFailed > 0) {
|
|
2494
2598
|
log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
|