dankgrinder 6.37.0 → 6.42.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/grinder.js CHANGED
@@ -5,7 +5,7 @@ const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/
5
5
  const rawLogger = require('./rawLogger');
6
6
  const {
7
7
  BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
8
- AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
8
+ AhoCorasick, LRUCache, StringPool, AsyncBatchQueue,
9
9
  } = require('./structures');
10
10
  const PKG_VERSION = require('../package.json').version;
11
11
 
@@ -94,6 +94,8 @@ const c = {
94
94
  };
95
95
 
96
96
  const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
97
+ // Unique marker written to stdout so we can query cursor position via DSR response
98
+ const MARKER = '\x1b[6n\x1b[@@MARKER@@';
97
99
  const DANK_MEMER_ID = '270904126974590976';
98
100
 
99
101
  // ── Safe options for search/crime ──────────────────────────
@@ -277,9 +279,9 @@ function boxTop(w, color) { return color + BOX.dtl + BOX.dh.repeat(w - 2) + BOX.
277
279
  function boxMid(w, color) { return color + BOX.tee + BOX.h.repeat(w - 2) + BOX.teeR + c.reset; }
278
280
  function boxBot(w, color) { return color + BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr + c.reset; }
279
281
  function boxLine(content, w, color) {
280
- const stripped = content.replace(/\x1b\[[0-9;]*m/g, '');
282
+ const stripped = stripAnsi(content);
281
283
  const pad = Math.max(0, w - 4 - stripped.length);
282
- return color + BOX.dv + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
284
+ return color + BOX.dv + c.reset + ' ' + stripAnsi(content) + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
283
285
  }
284
286
  function thinLine(w) { return ' ' + c.dim + BOX.h.repeat(w - 4) + c.reset; }
285
287
 
@@ -866,8 +868,8 @@ const CMD_NAMES_CLEAN = {
866
868
  bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
867
869
  ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
868
870
  hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
869
- tidy: 'Tidy', farm: 'Farm', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly',
870
- scratch: 'Scratch', adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
871
+ tidy: 'Tidy', farm: 'Farm', daily: 'Daily', monthly: 'Monthly',
872
+ adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
871
873
  drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
872
874
  work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
873
875
  };
@@ -1110,6 +1112,11 @@ class AccountWorker {
1110
1112
  this.commandQueue = null;
1111
1113
  this.lastHealthCheck = Date.now();
1112
1114
  this.doneToday = new Map();
1115
+
1116
+ // Dynamic cooldown learning: tracks actual parsed cooldowns per command.
1117
+ // Persisted in Redis hash `dkg:cd:learned:{accountId}` for cross-restart persistence.
1118
+ // Used as adaptive floor when command parsers don't return exact cooldowns.
1119
+ this._learnedCooldowns = new Map();
1113
1120
  this._fishRoundsSinceSell = 0;
1114
1121
  this._autoDepositThreshold = account.auto_deposit_threshold || 500000;
1115
1122
 
@@ -1483,7 +1490,9 @@ class AccountWorker {
1483
1490
  totalValue: result.totalValue || 0,
1484
1491
  }),
1485
1492
  });
1486
- } catch {}
1493
+ } catch (err) {
1494
+ console.error(`[${this.username}] inventory API error:`, err?.message || err);
1495
+ }
1487
1496
 
1488
1497
  return {
1489
1498
  ok: true,
@@ -1517,6 +1526,60 @@ class AccountWorker {
1517
1526
  }
1518
1527
  }
1519
1528
 
1529
+ // ── Profile Check ────────────────────────────────────────────
1530
+ async checkProfile(silent = false) {
1531
+ try {
1532
+ const result = await commands.runProfile({
1533
+ channel: this.channel,
1534
+ waitForDankMemer: (t) => this.waitForDankMemer(t),
1535
+ accountId: this.account.id,
1536
+ redis,
1537
+ });
1538
+
1539
+ if (result.error) {
1540
+ if (!silent) this.log('warn', `[profile] Check failed: ${result.error}`);
1541
+ return result;
1542
+ }
1543
+
1544
+ if (!silent) {
1545
+ this.log('info', `[profile] Level ${result.level} · ⏣ ${(result.wallet || 0).toLocaleString()} wallet · ⏣ ${(result.net || 0).toLocaleString()} net`);
1546
+ }
1547
+
1548
+ // Push to web dashboard via grinder status endpoint
1549
+ try {
1550
+ await fetch(`${API_URL}/api/grinder/profile`, {
1551
+ method: 'POST',
1552
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1553
+ body: JSON.stringify({
1554
+ account_id: this.account.id,
1555
+ level: result.level,
1556
+ xp_current: result.xpCurrent,
1557
+ xp_max: result.xpMax,
1558
+ wallet: result.wallet,
1559
+ bank: result.bank,
1560
+ net: result.net,
1561
+ items_unique: result.itemsUnique,
1562
+ items_total: result.itemsTotal,
1563
+ items_worth: result.itemsWorth,
1564
+ cmds_total: result.cmdsTotal,
1565
+ favorite: result.favorite,
1566
+ badges: result.badges || [],
1567
+ pets: result.pets || [],
1568
+ showcase: result.showcase || [],
1569
+ raw_text: result.rawText || '',
1570
+ }),
1571
+ });
1572
+ } catch (err) {
1573
+ console.error(`[${this.username}] profile API error:`, err?.message || err);
1574
+ }
1575
+
1576
+ return { ok: true, ...result };
1577
+ } catch (e) {
1578
+ if (!silent) this.log('warn', `[profile] Check error: ${e.message}`);
1579
+ return { ok: false, error: e.message };
1580
+ }
1581
+ }
1582
+
1520
1583
  async checkBalance(silent = false) {
1521
1584
  const prefix = this.account.use_slash ? '/' : 'pls';
1522
1585
  const sentAt = Date.now();
@@ -1717,22 +1780,48 @@ class AccountWorker {
1717
1780
  // Each modular command handler sends the command, waits for response,
1718
1781
  // handles Hold Tight / cooldowns / item-buying internally.
1719
1782
  async runCommand(cmdName, prefix) {
1720
- let cmdString;
1783
+ // Slash commands use '/cmd' without a space between slash and subcommand
1784
+ // (e.g. '/dep max', '/work shift'). Legacy prefix uses 'pls cmd'.
1785
+ const SLASH_CMD = new Map([
1786
+ ['dep max', '/dep max'],
1787
+ ['with max', '/with max'],
1788
+ ['work shift', '/work shift'],
1789
+ ['monthly', '/monthly'],
1790
+ ]);
1721
1791
  const bjBet = Math.max(5000, this.account.bet_amount || 5000);
1722
1792
  const gambBet = Math.max(10000, this.account.bet_amount || 10000);
1723
1793
 
1724
- switch (cmdName) {
1725
- case 'dep max': cmdString = `${prefix} dep max`; break;
1726
- case 'with max': cmdString = `${prefix} with max`; break;
1727
- case 'blackjack': cmdString = `${prefix} bj ${bjBet}`; break;
1728
- case 'cointoss': cmdString = `${prefix} cointoss ${gambBet}`; break;
1729
- case 'roulette': cmdString = `${prefix} roulette ${gambBet}`; break;
1730
- case 'slots': cmdString = `${prefix} slots ${gambBet}`; break;
1731
- case 'snakeeyes': cmdString = `${prefix} snakeeyes ${gambBet}`; break;
1732
- case 'work shift': cmdString = `${prefix} work shift`; break;
1733
- case 'weekly': cmdString = `${prefix} weekly`; break;
1734
- case 'monthly': cmdString = `${prefix} monthly`; break;
1735
- default: cmdString = `${prefix} ${cmdName}`;
1794
+ let cmdString;
1795
+ if (prefix === '/') {
1796
+ const slashVariant = SLASH_CMD.get(cmdName);
1797
+ if (slashVariant) {
1798
+ cmdString = slashVariant;
1799
+ } else if (cmdName === 'blackjack') {
1800
+ cmdString = `/bj ${bjBet}`;
1801
+ } else if (cmdName === 'cointoss') {
1802
+ cmdString = `/cointoss ${gambBet}`;
1803
+ } else if (cmdName === 'roulette') {
1804
+ cmdString = `/roulette ${gambBet}`;
1805
+ } else if (cmdName === 'slots') {
1806
+ cmdString = `/slots ${gambBet}`;
1807
+ } else if (cmdName === 'snakeeyes') {
1808
+ cmdString = `/snakeeyes ${gambBet}`;
1809
+ } else {
1810
+ cmdString = `/${cmdName}`;
1811
+ }
1812
+ } else {
1813
+ switch (cmdName) {
1814
+ case 'dep max': cmdString = `${prefix} dep max`; break;
1815
+ case 'with max': cmdString = `${prefix} with max`; break;
1816
+ case 'blackjack': cmdString = `${prefix} bj ${bjBet}`; break;
1817
+ case 'cointoss': cmdString = `${prefix} cointoss ${gambBet}`; break;
1818
+ case 'roulette': cmdString = `${prefix} roulette ${gambBet}`; break;
1819
+ case 'slots': cmdString = `${prefix} slots ${gambBet}`; break;
1820
+ case 'snakeeyes': cmdString = `${prefix} snakeeyes ${gambBet}`; break;
1821
+ case 'work shift': cmdString = `${prefix} work shift`; break;
1822
+ case 'monthly': cmdString = `${prefix} monthly`; break;
1823
+ default: cmdString = `${prefix} ${cmdName}`;
1824
+ }
1736
1825
  }
1737
1826
 
1738
1827
  if (shutdownCalled || !this.running) return;
@@ -1830,7 +1919,6 @@ class AccountWorker {
1830
1919
  case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
1831
1920
  case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
1832
1921
  case 'fish': cmdResult = await commands.runFish(cmdOpts); break;
1833
- case 'scratch': cmdResult = await commands.runScratch(cmdOpts); break;
1834
1922
  case 'adventure': cmdResult = await commands.runAdventure(cmdOpts); break;
1835
1923
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
1836
1924
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
@@ -1885,7 +1973,9 @@ class AccountWorker {
1885
1973
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1886
1974
  body: JSON.stringify({ account_id: this.account.id, active: false }),
1887
1975
  });
1888
- } catch {}
1976
+ } catch (err) {
1977
+ console.error(`[${this.username}] status deactivation API error:`, err?.message || err);
1978
+ }
1889
1979
  await sendLog(this.username, cmdString, 'VERIFICATION — account deactivated', 'error');
1890
1980
  sendWebhook('CAPTCHA ALERT', `**${this.username}** needs verification!\nCommand: \`${cmdName}\`\nSolve in Discord and re-enable from dashboard.`, 0xef4444);
1891
1981
  return;
@@ -1964,8 +2054,9 @@ class AccountWorker {
1964
2054
  const timeMatch = result.match(/<t:(\d+):R>/);
1965
2055
  let waitSec;
1966
2056
  if (timeMatch) {
1967
- const nextAvail = parseInt(timeMatch[1]) * 1000;
1968
- waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
2057
+ // Discord <t:TS:R> format: :R = relative seconds from NOW (not Unix ms).
2058
+ // The captured number IS already the number of seconds to wait.
2059
+ waitSec = Math.max(60, parseInt(timeMatch[1]));
1969
2060
  } else {
1970
2061
  const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
1971
2062
  waitSec = defaultWaits[cmdName] || 86400;
@@ -1995,6 +2086,11 @@ class AccountWorker {
1995
2086
  if (cmdResult.nextCooldownSec) {
1996
2087
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1997
2088
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
2089
+ // Learn: record this cooldown as the known value for future fallback use
2090
+ this._learnedCooldowns.set(cmdName, cmdResult.nextCooldownSec);
2091
+ if (redis) {
2092
+ await redis.hset(`dkg:cd:learned:${this.account.id}`, cmdName, String(cmdResult.nextCooldownSec));
2093
+ }
1998
2094
  }
1999
2095
 
2000
2096
  // Smart gambling loss tracker
@@ -2115,19 +2211,38 @@ class AccountWorker {
2115
2211
  }
2116
2212
  }
2117
2213
 
2214
+ // Load previously learned cooldowns from Redis so floors adapt across restarts.
2215
+ async _loadLearnedCooldowns() {
2216
+ if (!redis) return;
2217
+ try {
2218
+ const learned = await redis.hgetall(`dkg:cd:learned:${this.account.id}`);
2219
+ for (const [cmd, val] of Object.entries(learned)) {
2220
+ const n = parseFloat(val);
2221
+ if (Number.isFinite(n) && n > 0) {
2222
+ this._learnedCooldowns.set(cmd, n);
2223
+ }
2224
+ }
2225
+ if (this._learnedCooldowns.size > 0) {
2226
+ this.log('info', `Loaded ${this._learnedCooldowns.size} learned cooldowns from Redis`);
2227
+ }
2228
+ } catch (e) {
2229
+ this.log('warn', `Failed to load learned cooldowns: ${e.message}`);
2230
+ }
2231
+ }
2232
+
2118
2233
  printStats() {
2119
2234
  // Stats are shown in the live dashboard, no-op here
2120
2235
  }
2121
2236
 
2122
2237
  // ── Command Map (shared across ticks, used to build the heap) ──
2123
2238
  // Priority: higher = runs first when multiple commands are ready simultaneously.
2124
- // 10 = time-gated (daily/weekly/monthly — never miss),
2239
+ // 10 = time-gated (daily/monthly — never miss),
2125
2240
  // 8 = financial safety (deposit),
2126
2241
  // 7 = gambling fast-cycle (2-3s CD — run MOST often),
2127
2242
  // 6 = fast grinders (10s CD),
2128
2243
  // 5 = medium grinders (20-40s CD),
2129
2244
  // 4 = resource grinders (hunt/dig — need items),
2130
- // 3 = interactive/long CD (adventure/stream/work/scratch),
2245
+ // 3 = interactive/long CD (adventure/stream/work),
2131
2246
  // 2 = utility (drops/use/tidy)
2132
2247
  static COMMAND_MAP = [
2133
2248
  // Gambling — 2-3s CD, highest frequency
@@ -2138,7 +2253,7 @@ class AccountWorker {
2138
2253
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
2139
2254
  // Fast grinders — 10s CD
2140
2255
  { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
2141
- { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
2256
+ { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
2142
2257
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
2143
2258
  { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
2144
2259
  // Medium grinders — 20-25s CD
@@ -2149,24 +2264,23 @@ class AccountWorker {
2149
2264
  { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 5 },
2150
2265
  // Slow grinders — 40s CD
2151
2266
  { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
2152
- { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
2153
- { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
2267
+ { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
2268
+ { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
2154
2269
  // Interactive — response-driven CD (handler sets nextCooldownSec)
2155
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
2156
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
2157
- // scratch removed requires voting which can't be automated
2158
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
2270
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
2271
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
2272
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
2159
2273
  // Time-gated (run ASAP when available)
2160
- { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
2161
- // weekly removed — premium only, not available for free users
2162
- { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000,priority: 10 },
2274
+ { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
2275
+ // monthly — premium only
2276
+ { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
2163
2277
  // Financial safety
2164
- { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
2165
- { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
2278
+ { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
2279
+ { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
2166
2280
  // Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
2167
2281
  ].map(Object.freeze);
2168
2282
 
2169
- buildCommandQueue() {
2283
+ async buildCommandQueue() {
2170
2284
  const heap = new MinHeap();
2171
2285
  const now = Date.now();
2172
2286
  let enabled = AccountWorker.COMMAND_MAP.filter(
@@ -2180,8 +2294,33 @@ class AccountWorker {
2180
2294
  enabled = AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key]));
2181
2295
  }
2182
2296
 
2297
+ // Restore cooldown state from Redis so commands don't re-run immediately
2298
+ // after restart. We use TTL to calculate remaining cooldown time.
2299
+ const accountId = this.account.id;
2300
+ const cmdKeys = enabled.map(info => `dkg:cd:${accountId}:${info.cmd}`);
2301
+ let ttlMap = new Map();
2302
+ if (redis) {
2303
+ const pipeline = redis.pipeline();
2304
+ for (const k of cmdKeys) pipeline.ttl(k);
2305
+ const results = await pipeline.exec();
2306
+ for (let i = 0; i < cmdKeys.length; i++) {
2307
+ const [err, val] = results[i];
2308
+ if (!err && Number.isFinite(val) && val > 0) {
2309
+ ttlMap.set(cmdKeys[i], val);
2310
+ }
2311
+ }
2312
+ }
2313
+
2183
2314
  for (const info of enabled) {
2184
- heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
2315
+ let nextRunAt = now;
2316
+ const key = `dkg:cd:${accountId}:${info.cmd}`;
2317
+ const ttlVal = ttlMap.get(key);
2318
+ if (Number.isFinite(ttlVal) && ttlVal > 0) {
2319
+ nextRunAt = now + ttlVal * 1000;
2320
+ this._cooldownBloom.add(key);
2321
+ this.log('info', `Restored cooldown for ${info.cmd}: ${ttlVal}s remaining`);
2322
+ }
2323
+ heap.push({ cmd: info.cmd, nextRunAt, priority: info.priority, info });
2185
2324
  }
2186
2325
  return heap;
2187
2326
  }
@@ -2309,7 +2448,7 @@ class AccountWorker {
2309
2448
 
2310
2449
  // Resume grind loop if it was running
2311
2450
  if (!this.commandQueue || this.commandQueue.size === 0) {
2312
- this.commandQueue = this.buildCommandQueue();
2451
+ this.commandQueue = await this.buildCommandQueue();
2313
2452
  }
2314
2453
  } else {
2315
2454
  this.log('error', 'Recovered connection but channel not found — retrying');
@@ -2407,98 +2546,65 @@ class AccountWorker {
2407
2546
  }
2408
2547
 
2409
2548
  if (!this.commandQueue || this.commandQueue.size === 0) {
2410
- this.commandQueue = this.buildCommandQueue();
2549
+ this.commandQueue = await this.buildCommandQueue();
2411
2550
  }
2412
2551
  if (!this.commandQueue || this.commandQueue.size === 0) {
2413
2552
  this.tickTimeout = setTimeout(() => this.tick(), 15000);
2414
2553
  return;
2415
2554
  }
2416
2555
 
2417
- const top = this.commandQueue.peek();
2418
- if (top.nextRunAt > now) {
2419
- const waitMs = Math.min(top.nextRunAt - now, 2000);
2420
- this.setStatus('cooldown...');
2421
- this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2422
- return;
2423
- }
2424
-
2425
- const item = this.commandQueue.pop();
2426
- if (!item) {
2427
- this.tickTimeout = setTimeout(() => this.tick(), 1000);
2428
- return;
2556
+ // ── Scan entire queue for ready commands ─────────────────────
2557
+ // Previously we only peeked the top item, missing other commands that
2558
+ // were already ready while a slower command was at the top of the heap.
2559
+ // Now we drain the queue and separate ready vs. waiting commands.
2560
+ // All ready commands fire immediately — no priority, FIFO order.
2561
+ const readyItems = [];
2562
+ const waitingItems = [];
2563
+ while (this.commandQueue.size > 0) {
2564
+ const item = this.commandQueue.pop();
2565
+ if (!item) break;
2566
+ if (item.nextRunAt <= now) {
2567
+ readyItems.push(item);
2568
+ } else {
2569
+ waitingItems.push(item);
2570
+ }
2429
2571
  }
2430
2572
 
2431
- const ready = await this.isCooldownReady(item.cmd);
2432
- if (!ready) {
2433
- const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
2434
- item.nextRunAt = now + cd * 1000;
2435
- if (this.commandQueue) this.commandQueue.push(item);
2436
- this.tickTimeout = setTimeout(() => this.tick(), 100);
2437
- return;
2573
+ // Re-insert waiting items back into the queue (they still have time to wait)
2574
+ for (const item of waitingItems) {
2575
+ this.commandQueue.push(item);
2438
2576
  }
2439
2577
 
2440
- // Skip time-gated commands if already claimed (in-memory + Redis)
2441
- if (item.cmd === 'daily' || item.cmd === 'weekly' || item.cmd === 'monthly' || item.cmd === 'drops') {
2442
- const memExpiry = this.doneToday.get(item.cmd);
2443
- if (memExpiry && Date.now() < memExpiry) {
2444
- item.nextRunAt = memExpiry;
2445
- if (this.commandQueue) this.commandQueue.push(item);
2446
- this.tickTimeout = setTimeout(() => this.tick(), 100);
2447
- return;
2448
- }
2449
- if (redis) {
2450
- try {
2451
- const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
2452
- if (done) {
2453
- const ttlMap = { daily: 86400, weekly: 604800, monthly: 2592000, drops: 86400 };
2454
- const ttl = ttlMap[item.cmd] || 86400;
2455
- const expiry = now + ttl * 1000;
2456
- this.doneToday.set(item.cmd, expiry);
2457
- item.nextRunAt = expiry;
2458
- if (this.commandQueue) this.commandQueue.push(item);
2459
- this.tickTimeout = setTimeout(() => this.tick(), 100);
2460
- return;
2461
- }
2462
- } catch {}
2578
+ if (readyItems.length === 0) {
2579
+ // Nothing ready wait until the soonest command becomes available.
2580
+ let minWaitMs = 5000; // cap at 5s to stay responsive
2581
+ for (const item of waitingItems) {
2582
+ const diff = item.nextRunAt - now;
2583
+ if (diff < minWaitMs) minWaitMs = diff;
2463
2584
  }
2464
- }
2465
-
2466
- // Smart gambling: skip gamble commands while loss-paused
2467
- const GAMBLE_SET = new Set(['blackjack', 'cointoss', 'roulette', 'slots', 'snakeeyes']);
2468
- if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > now) {
2469
- item.nextRunAt = this._gamblePausedUntil;
2470
- if (this.commandQueue) this.commandQueue.push(item);
2471
- this.setStatus(`gamble paused (${Math.ceil((this._gamblePausedUntil - now) / 1000)}s)`);
2472
- this.tickTimeout = setTimeout(() => this.tick(), 100);
2585
+ this.setStatus('cooldown...');
2586
+ this.tickTimeout = setTimeout(() => this.tick(), Math.max(100, Math.min(minWaitMs, 5000)));
2473
2587
  return;
2474
2588
  }
2475
- if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > 0 && this._gamblePausedUntil <= now) {
2476
- this._gamblePausedUntil = 0;
2477
- this._gambleLossStreak = 0;
2478
- this._gambleSessionLoss = 0;
2479
- this.log('info', 'Gambling pause expired — resuming bets');
2480
- }
2481
2589
 
2482
- // TokenBucket rate limiter: prevent Discord 429s by throttling commands
2483
- if (!this._rateLimiter.consume(1)) {
2484
- const waitMs = this._rateLimiter.waitTime(1);
2485
- this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
2486
- if (this.commandQueue) this.commandQueue.push(item);
2487
- this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2488
- return;
2590
+ // FIFO execute commands in the order they became ready.
2591
+ // Commands that aren't picked this tick go back into the queue for next tick.
2592
+ const item = readyItems[0];
2593
+
2594
+ // Any remaining ready items that we didn't execute go back immediately
2595
+ // so they run in the next tick without waiting for the slow top item.
2596
+ for (let i = 1; i < readyItems.length; i++) {
2597
+ this.commandQueue.push(readyItems[i]);
2489
2598
  }
2490
2599
 
2491
- this._cmdRate.increment();
2492
- this.busy = true;
2600
+ // Anti-detection: per-account jitter + micro-pauses for this command
2493
2601
  const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
2494
- // Anti-detection: per-account jitter with varying patterns
2495
2602
  const patternMod = this._activePattern;
2496
2603
  const jitterBase = cd <= 5
2497
2604
  ? 0.3 + Math.random() * 0.7
2498
2605
  : cd <= 20
2499
2606
  ? 0.5 + Math.random() * 1.5
2500
2607
  : 1 + Math.random() * 2;
2501
- // Add human-like micro-pauses (occasionally take longer, simulating distraction)
2502
2608
  const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
2503
2609
  const totalWait = cd + jitterBase + microPause;
2504
2610
 
@@ -2510,6 +2616,20 @@ class AccountWorker {
2510
2616
  await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
2511
2617
  }
2512
2618
 
2619
+ // TokenBucket rate limiter — prevent Discord 429s
2620
+ if (!this._rateLimiter.consume(1)) {
2621
+ const waitMs = this._rateLimiter.waitTime(1);
2622
+ this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
2623
+ if (this.commandQueue) this.commandQueue.push(item);
2624
+ this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2625
+ return;
2626
+ }
2627
+
2628
+ this.busy = true;
2629
+
2630
+ // ── Run command (with interactive retry) ───────────────────
2631
+ // Commands run ONE BY ONE — sequential execution, no concurrency within this account.
2632
+ // Each runCommand() call waits for Dank Memer's Discord response before returning.
2513
2633
  const prefix = this.account.use_slash ? '/' : 'pls';
2514
2634
  this.setStatus(formatCommandName(item.cmd));
2515
2635
 
@@ -2523,15 +2643,40 @@ class AccountWorker {
2523
2643
  next_run_at: nextItemRun?.nextRunAt || null,
2524
2644
  });
2525
2645
 
2526
- const beforeCoins = this.stats.coins;
2527
- await this.runCommand(item.cmd, prefix);
2528
- const earned = this.stats.coins - beforeCoins;
2646
+ // Interactive commands (button-click): retry up to 3 times on failure.
2647
+ // Non-interactive commands run once.
2648
+ const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
2649
+ const isInteractive = INTERACTIVE_CMDS.has(item.cmd);
2650
+ const maxAttempts = isInteractive ? 3 : 1;
2651
+ let attempt = 0;
2652
+ let lastError = null;
2653
+ let earned = 0;
2654
+ while (attempt < maxAttempts) {
2655
+ attempt++;
2656
+ try {
2657
+ const beforeCoins = this.stats.coins;
2658
+ await this.runCommand(item.cmd, prefix);
2659
+ earned = this.stats.coins - beforeCoins;
2660
+ lastError = null;
2661
+ this.lastCommandRun = Date.now();
2662
+ break; // success
2663
+ } catch (err) {
2664
+ lastError = err;
2665
+ this.log('warn', `${item.cmd} attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
2666
+ if (attempt < maxAttempts) {
2667
+ await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));
2668
+ }
2669
+ }
2670
+ }
2671
+ if (lastError) {
2672
+ this.log('error', `${item.cmd} failed after ${maxAttempts} attempts — skipping`);
2673
+ }
2529
2674
 
2530
- // Grace period for interactive (button-click) commands — Dank Memer
2531
- // needs time to process the interaction before accepting the next command.
2532
- // Without this, the next command gets "Hold Tight" errors.
2533
- const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
2534
- if (INTERACTIVE_CMDS.has(item.cmd)) {
2675
+ this._cmdRate.increment();
2676
+
2677
+ // Grace period for interactive commands Dank Memer needs time to process
2678
+ // the interaction before accepting the next command.
2679
+ if (isInteractive) {
2535
2680
  await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
2536
2681
  }
2537
2682
 
@@ -2549,8 +2694,6 @@ class AccountWorker {
2549
2694
  this.failStreak = 0;
2550
2695
  }
2551
2696
 
2552
- this.lastCommandRun = Date.now();
2553
-
2554
2697
  // Exponential backoff: if too many consecutive failures, slow down
2555
2698
  const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
2556
2699
  // Minimum 5s cooldown for failed commands to prevent rapid-fire retries
@@ -2558,22 +2701,33 @@ class AccountWorker {
2558
2701
 
2559
2702
  if (this.commandQueue && this.running && !shutdownCalled) {
2560
2703
  const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
2561
- let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
2562
2704
  this._lastCooldownOverride = null;
2563
2705
 
2564
- // Smart fallback floors for long/interactive commands when parser misses exact cooldown.
2565
- if (!hasOverride) {
2566
- const floor = AccountWorker.SMART_CD_FLOORS[item.cmd];
2567
- if (Number.isFinite(floor) && floor > 0) {
2568
- effectiveWait = Math.max(effectiveWait, floor);
2706
+ let scheduledWaitSec;
2707
+ if (hasOverride) {
2708
+ // Exact cooldown returned by command parser — use it without jitter or backoff.
2709
+ // This ensures work (1h), adventure (5h), stream (10m), farm (varies) are honored exactly.
2710
+ scheduledWaitSec = Math.max(1, this._lastCooldownOverride);
2711
+ } else {
2712
+ // Jitter-based cooldown for commands without a parsed override.
2713
+ let effectiveWait = totalWait;
2714
+
2715
+ // Smart fallback floors: static floor OR dynamically learned cooldown (whichever is higher).
2716
+ // Learned cooldowns come from actual parsed responses and persist across restarts via Redis.
2717
+ const staticFloor = AccountWorker.SMART_CD_FLOORS[item.cmd] || 0;
2718
+ const learnedCd = this._learnedCooldowns.get(item.cmd) || 0;
2719
+ const adaptiveFloor = Math.max(staticFloor, learnedCd);
2720
+ if (adaptiveFloor > 0) {
2721
+ effectiveWait = Math.max(effectiveWait, adaptiveFloor);
2569
2722
  }
2570
- }
2571
2723
 
2572
- if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
2573
- effectiveWait = MIN_FAIL_COOLDOWN;
2724
+ if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
2725
+ effectiveWait = MIN_FAIL_COOLDOWN;
2726
+ }
2727
+
2728
+ scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
2574
2729
  }
2575
2730
 
2576
- const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
2577
2731
  await this.setCooldown(item.cmd, scheduledWaitSec);
2578
2732
  item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
2579
2733
  this.commandQueue.push(item);
@@ -2611,7 +2765,8 @@ class AccountWorker {
2611
2765
  this.failStreak = 0;
2612
2766
  this.cycleCount = 0;
2613
2767
  this.lastCommandRun = 0;
2614
- this.commandQueue = this.buildCommandQueue();
2768
+ await this._loadLearnedCooldowns();
2769
+ this.commandQueue = await this.buildCommandQueue();
2615
2770
  this.lastHealthCheck = Date.now();
2616
2771
 
2617
2772
  // Reactive alert listener: run `pls alert` only when Dank Memer
@@ -2713,11 +2868,28 @@ class AccountWorker {
2713
2868
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2714
2869
  body: JSON.stringify({ action_id: action.id }),
2715
2870
  });
2716
- } catch {}
2871
+ } catch (err) {
2872
+ console.error(`[${this.username}] delete pending action error:`, err?.message || err);
2873
+ }
2874
+ }
2875
+ if (action.action === 'check_profile' && !this.busy) {
2876
+ this.log('info', 'Dashboard requested profile check');
2877
+ await this.checkProfile().catch(() => {});
2878
+ try {
2879
+ await fetch(`${API_URL}/api/grinder/actions`, {
2880
+ method: 'DELETE',
2881
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2882
+ body: JSON.stringify({ action_id: action.id }),
2883
+ });
2884
+ } catch (err) {
2885
+ console.error(`[${this.username}] delete pending action error:`, err?.message || err);
2886
+ }
2717
2887
  }
2718
2888
  }
2719
2889
  }
2720
- } catch { /* silent */ }
2890
+ } catch (err) {
2891
+ console.error(`[${this.username}] refreshConfig error:`, err?.message || err);
2892
+ }
2721
2893
  }
2722
2894
 
2723
2895
  async start() {
@@ -2767,9 +2939,8 @@ class AccountWorker {
2767
2939
  { key: 'cmd_fish', l: 'fish' }, { key: 'cmd_beg', l: 'beg' },
2768
2940
  { key: 'cmd_search', l: 'search' }, { key: 'cmd_hl', l: 'hl' },
2769
2941
  { key: 'cmd_crime', l: 'crime' }, { key: 'cmd_pm', l: 'pm' },
2770
- { key: 'cmd_daily', l: 'daily' }, { key: 'cmd_weekly', l: 'weekly' },
2771
- { key: 'cmd_monthly', l: 'monthly' }, { key: 'cmd_work', l: 'work' },
2772
- { key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
2942
+ { key: 'cmd_daily', l: 'daily' }, { key: 'cmd_monthly', l: 'monthly' },
2943
+ { key: 'cmd_work', l: 'work' }, { key: 'cmd_stream', l: 'stream' },
2773
2944
  { key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
2774
2945
  { key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
2775
2946
  { key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
@@ -3048,9 +3219,30 @@ async function start(apiKey, apiUrl) {
3048
3219
  }
3049
3220
  loginLines.push(` ${'─'.repeat(loginVis)}`);
3050
3221
  for (const l of loginLines) console.log(l);
3051
- loginLines = null;
3222
+
3223
+ // Dynamically capture the starting row of the login table via DSR
3224
+ let loginBaseRow = 1;
3225
+ const captureLoginRow = () => new Promise(resolve => {
3226
+ process.stdout.write(MARKER);
3227
+ const chunks = [];
3228
+ const handler = (chunk) => {
3229
+ chunks.push(chunk);
3230
+ const raw = chunks.join('');
3231
+ const m = raw.match(/\x1b\[(\d+);\d+R/);
3232
+ if (m) {
3233
+ process.stdin.removeListener('data', handler);
3234
+ loginBaseRow = parseInt(m[1], 10) + 1;
3235
+ resolve();
3236
+ }
3237
+ };
3238
+ process.stdin.on('data', handler);
3239
+ setTimeout(resolve, 50);
3240
+ });
3241
+ await captureLoginRow();
3052
3242
 
3053
3243
  let loginPending = new Array(accounts.length).fill(true);
3244
+ const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3245
+
3054
3246
  const drawLoginSpinners = () => {
3055
3247
  for (let i = 0; i < loginPending.length; i++) {
3056
3248
  if (!loginPending[i]) continue;
@@ -3059,8 +3251,13 @@ async function start(apiKey, apiUrl) {
3059
3251
  const name = loginStates[i].name.substring(0, colName).padEnd(colName);
3060
3252
  const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
3061
3253
  const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3062
- process.stdout.write(`\r\x1b[2K ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
3254
+ const row = loginBaseRow + 1 + i; // +1 skips the top border line
3255
+ moveToRow(row);
3256
+ process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
3063
3257
  }
3258
+ // Move cursor back to bottom to avoid overwriting the bottom border
3259
+ const lastRow = loginBaseRow + 1 + accounts.length + 1;
3260
+ moveToRow(lastRow);
3064
3261
  };
3065
3262
  const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3066
3263
 
@@ -3089,7 +3286,9 @@ async function start(apiKey, apiUrl) {
3089
3286
  guild = 'timeout'.padEnd(colGuild);
3090
3287
  cmds = '···'.padEnd(colCmds);
3091
3288
  }
3092
- process.stdout.write(`\r\x1b[2K ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3289
+ const row = loginBaseRow + 1 + idx; // +1 skips the top border line
3290
+ moveToRow(row);
3291
+ process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3093
3292
  };
3094
3293
 
3095
3294
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
@@ -3137,6 +3336,28 @@ async function start(apiKey, apiUrl) {
3137
3336
  const iColVal = 16;
3138
3337
  const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3139
3338
 
3339
+ // Print a unique marker, query its position, then overwrite it with the table
3340
+ process.stdout.write(MARKER);
3341
+ let invBaseRow = 1;
3342
+ const captureRow = () => new Promise(resolve => {
3343
+ const chunks = [];
3344
+ const handler = (chunk) => {
3345
+ chunks.push(chunk);
3346
+ const raw = chunks.join('');
3347
+ const m = raw.match(/\x1b\[(\d+);\d+R/);
3348
+ if (m) {
3349
+ process.stdin.removeListener('data', handler);
3350
+ invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
3351
+ resolve();
3352
+ }
3353
+ };
3354
+ process.stdin.on('data', handler);
3355
+ setTimeout(resolve, 50);
3356
+ });
3357
+ await captureRow();
3358
+
3359
+ // Now print the inventory table starting at invBaseRow
3360
+ const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3140
3361
  console.log(` ${'─'.repeat(invVis)}`);
3141
3362
  for (let i = 0; i < activeWorkers.length; i++) {
3142
3363
  const w = activeWorkers[i];
@@ -3155,7 +3376,8 @@ async function start(apiKey, apiUrl) {
3155
3376
  const filled = Math.round(pct * barW);
3156
3377
  const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3157
3378
  const pctStr = `${Math.round(pct * 100)}%`;
3158
- process.stdout.write(`\r\x1b[2K ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - invPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}${pctStr}${c.reset} `);
3379
+ invMoveToRow(invBaseRow);
3380
+ process.stdout.write(` ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - invPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}${pctStr}${c.reset} \x1b[K`);
3159
3381
  };
3160
3382
  const invSpinnerInterval = setInterval(drawInvProgress, 80);
3161
3383
 
@@ -3171,7 +3393,9 @@ async function start(apiKey, apiUrl) {
3171
3393
  const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3172
3394
  const itemStr = `${items}`.padEnd(iColItems);
3173
3395
  const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3174
- process.stdout.write(`\r\x1b[2K ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3396
+ const row = invBaseRow + 1 + i;
3397
+ invMoveToRow(row);
3398
+ process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3175
3399
  if (invRes?.ok) invDone++; else invFailed++;
3176
3400
  }));
3177
3401
 
@@ -3195,6 +3419,27 @@ async function start(apiKey, apiUrl) {
3195
3419
  const bColLs = 4;
3196
3420
  const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3197
3421
 
3422
+ // Capture starting row for balance phase
3423
+ process.stdout.write(MARKER);
3424
+ let balBaseRow = 1;
3425
+ const balCaptureRow = () => new Promise(resolve => {
3426
+ const chunks = [];
3427
+ const handler = (chunk) => {
3428
+ chunks.push(chunk);
3429
+ const raw = chunks.join('');
3430
+ const m = raw.match(/\x1b\[(\d+);\d+R/);
3431
+ if (m) {
3432
+ process.stdin.removeListener('data', handler);
3433
+ balBaseRow = parseInt(m[1], 10) + 1;
3434
+ resolve();
3435
+ }
3436
+ };
3437
+ process.stdin.on('data', handler);
3438
+ setTimeout(resolve, 50);
3439
+ });
3440
+ await balCaptureRow();
3441
+
3442
+ const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3198
3443
  console.log(` ${'─'.repeat(balVis)}`);
3199
3444
  for (let i = 0; i < activeWorkers.length; i++) {
3200
3445
  const w = activeWorkers[i];
@@ -3212,7 +3457,8 @@ async function start(apiKey, apiUrl) {
3212
3457
  const barW = Math.min(20, startupTw - 40);
3213
3458
  const filled = Math.round(pct * barW);
3214
3459
  const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3215
- process.stdout.write(`\r\x1b[2K ${rgb(251, 191, 36)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - balPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} `);
3460
+ balMoveToRow(balBaseRow);
3461
+ process.stdout.write(` ${rgb(251, 191, 36)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - balPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} \x1b[K`);
3216
3462
  };
3217
3463
  const balSpinnerInterval = setInterval(drawBalProgress, 80);
3218
3464
 
@@ -3228,7 +3474,9 @@ async function start(apiKey, apiUrl) {
3228
3474
  const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3229
3475
  const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3230
3476
  const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3231
- process.stdout.write(`\r\x1b[2K ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}\x1b[K`);
3477
+ const row = balBaseRow + 1 + i;
3478
+ balMoveToRow(row);
3479
+ process.stdout.write(` ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}\x1b[K`);
3232
3480
  balDone++;
3233
3481
  }));
3234
3482