dankgrinder 6.34.0 → 6.39.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.
@@ -68,11 +68,30 @@ function parseFarmCooldownSec(text) {
68
68
  const hasCooldownContext = /easy tiger|slow it down|already farmed|farm again|cooldown|can be used again|on cooldown/.test(lower);
69
69
  if (!hasCooldownContext) return null;
70
70
 
71
- const ts = clean.match(RE_TS);
72
- if (ts) {
73
- const diff = parseInt(ts[1], 10) - Math.floor(Date.now() / 1000);
74
- if (diff > 0) return diff;
71
+ // Handle all Discord timestamp formats: :R, :f, :F, :T, :t, :d, :D
72
+ // :R = relative seconds from now (already seconds since epoch difference)
73
+ // :f/:F = absolute datetime diff from now
74
+ // :T/:t/:d/:D = not useful for cooldowns (no time component)
75
+ const RE_TS_ALL = /<t:(\d+):([tTdDfFR])>/g;
76
+ const now = Math.floor(Date.now() / 1000);
77
+ let best = null;
78
+ for (const m of clean.matchAll(RE_TS_ALL)) {
79
+ const ts = parseInt(m[1], 10);
80
+ const fmt = m[2];
81
+ if (!Number.isFinite(ts)) continue;
82
+ let diff;
83
+ if (fmt === 'R') {
84
+ diff = ts - now; // :R is already relative seconds
85
+ } else if (fmt === 'f' || fmt === 'F' || fmt === 'T') {
86
+ diff = ts - now; // :f/:F/:T are absolute → diff gives seconds remaining
87
+ } else {
88
+ // :t/:d/:D don't have enough info for cooldown calc — skip
89
+ continue;
90
+ }
91
+ if (diff > 0 && (best === null || diff < best)) best = diff;
75
92
  }
93
+ if (best !== null) return Math.max(5, best);
94
+
76
95
  const mm = clean.match(RE_MIN);
77
96
  if (mm) return parseInt(mm[1], 10) * 60;
78
97
  const hh = clean.match(RE_HR);
@@ -1436,10 +1455,10 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1436
1455
  channel, waitForDankMemer, response: cycleResponse,
1437
1456
  button: allBtn, tag: `farm-cycle-${action}-apply`,
1438
1457
  });
1439
- if (!applyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
1458
+ if (!lastApplyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
1440
1459
 
1441
1460
  // Step 5: advance past any confirmation screens back to the manage menu
1442
- cycleResponse = await advancePastConfirmation(applyResp, waitForDankMemer);
1461
+ cycleResponse = await advancePastConfirmation(lastApplyResp, waitForDankMemer);
1443
1462
  if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
1444
1463
  actionsTaken++;
1445
1464
  lastAction = action;
@@ -1589,16 +1608,23 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
1589
1608
  LOG.coin(`[farm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
1590
1609
  // After a harvest, Dank Memer auto-plants new crops. Record harvest time
1591
1610
  // in Redis so the next run knows when crops should be ready.
1592
- // Also: if the farm shows "empty" or "manage" state (no grow timestamp),
1593
- // set a short re-check window instead of trusting a potentially stale grow queue.
1611
+ // Only force a short 30s re-check if there is NO known grow queue timer.
1612
+ // If growReadyEnd is set (e.g. "ready in 26m"), that takes priority.
1594
1613
  const afterHarvestState = analyzeFarmState({ msg: cycleResponse, text });
1595
1614
  const farmIsHarvested = afterHarvestState.stage === 'overview'
1596
1615
  || /seems? pretty empty|empty\.{0,3}|hoe|water|plant|harvest/i.test(text);
1597
1616
  if (farmIsHarvested) {
1598
- // Farm was harvested and is back to manage/empty state.
1599
- // Re-check in 30s to start the next cycle (hoe→water→plant→harvest).
1600
- nextCd = Math.min(nextCd, 30);
1601
- LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage} re-checking in 30s`);
1617
+ const hasGrowTimer = Number.isFinite(growReadyEnd) && growReadyEnd > 0;
1618
+ if (hasGrowTimer) {
1619
+ // Crops were planted and a grow queue timer exists (e.g. "ready in 26m").
1620
+ // Respect that timer the 30s cap should NOT override it.
1621
+ LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage}, grow in ${Math.ceil(growReadyEnd / 60)}m — waiting for grow queue`);
1622
+ } else {
1623
+ // No grow queue timer visible (farm is truly empty or manage menu without timestamp).
1624
+ // Short re-check to start the next hoe→water→plant→harvest cycle.
1625
+ nextCd = Math.min(nextCd, 30);
1626
+ LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage} — re-checking in 30s`);
1627
+ }
1602
1628
  }
1603
1629
  // Record in Redis for cross-instance awareness
1604
1630
  let growDurMs = null;
@@ -206,7 +206,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
206
206
 
207
207
  if (isHoldTight(response)) {
208
208
  const reason = getHoldTightReason(response);
209
- return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
209
+ return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason, nextCooldownSec: 30 };
210
210
  }
211
211
 
212
212
  await hydrate(response);
@@ -418,7 +418,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
418
418
  const reason = getHoldTightReason(current);
419
419
  LOG.warn(`[work] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
420
420
  await sleep(30000);
421
- return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
421
+ return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason, nextCooldownSec: 30 };
422
422
  }
423
423
 
424
424
  if (isCV2(current)) await ensureCV2(current);
package/lib/grinder.js CHANGED
@@ -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 ──────────────────────────
@@ -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
 
@@ -1995,6 +2002,11 @@ class AccountWorker {
1995
2002
  if (cmdResult.nextCooldownSec) {
1996
2003
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1997
2004
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
2005
+ // Learn: record this cooldown as the known value for future fallback use
2006
+ this._learnedCooldowns.set(cmdName, cmdResult.nextCooldownSec);
2007
+ if (redis) {
2008
+ await redis.hset(`dkg:cd:learned:${this.account.id}`, cmdName, String(cmdResult.nextCooldownSec));
2009
+ }
1998
2010
  }
1999
2011
 
2000
2012
  // Smart gambling loss tracker
@@ -2115,6 +2127,25 @@ class AccountWorker {
2115
2127
  }
2116
2128
  }
2117
2129
 
2130
+ // Load previously learned cooldowns from Redis so floors adapt across restarts.
2131
+ async _loadLearnedCooldowns() {
2132
+ if (!redis) return;
2133
+ try {
2134
+ const learned = await redis.hgetall(`dkg:cd:learned:${this.account.id}`);
2135
+ for (const [cmd, val] of Object.entries(learned)) {
2136
+ const n = parseFloat(val);
2137
+ if (Number.isFinite(n) && n > 0) {
2138
+ this._learnedCooldowns.set(cmd, n);
2139
+ }
2140
+ }
2141
+ if (this._learnedCooldowns.size > 0) {
2142
+ this.log('info', `Loaded ${this._learnedCooldowns.size} learned cooldowns from Redis`);
2143
+ }
2144
+ } catch (e) {
2145
+ this.log('warn', `Failed to load learned cooldowns: ${e.message}`);
2146
+ }
2147
+ }
2148
+
2118
2149
  printStats() {
2119
2150
  // Stats are shown in the live dashboard, no-op here
2120
2151
  }
@@ -2166,7 +2197,7 @@ class AccountWorker {
2166
2197
  // Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
2167
2198
  ].map(Object.freeze);
2168
2199
 
2169
- buildCommandQueue() {
2200
+ async buildCommandQueue() {
2170
2201
  const heap = new MinHeap();
2171
2202
  const now = Date.now();
2172
2203
  let enabled = AccountWorker.COMMAND_MAP.filter(
@@ -2180,8 +2211,33 @@ class AccountWorker {
2180
2211
  enabled = AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key]));
2181
2212
  }
2182
2213
 
2214
+ // Restore cooldown state from Redis so commands don't re-run immediately
2215
+ // after restart. We use TTL to calculate remaining cooldown time.
2216
+ const accountId = this.account.id;
2217
+ const cmdKeys = enabled.map(info => `dkg:cd:${accountId}:${info.cmd}`);
2218
+ let ttlMap = new Map();
2219
+ if (redis) {
2220
+ const pipeline = redis.pipeline();
2221
+ for (const k of cmdKeys) pipeline.ttl(k);
2222
+ const results = await pipeline.exec();
2223
+ for (let i = 0; i < cmdKeys.length; i++) {
2224
+ const [err, val] = results[i];
2225
+ if (!err && Number.isFinite(val) && val > 0) {
2226
+ ttlMap.set(cmdKeys[i], val);
2227
+ }
2228
+ }
2229
+ }
2230
+
2183
2231
  for (const info of enabled) {
2184
- heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
2232
+ let nextRunAt = now;
2233
+ const key = `dkg:cd:${accountId}:${info.cmd}`;
2234
+ const ttlVal = ttlMap.get(key);
2235
+ if (Number.isFinite(ttlVal) && ttlVal > 0) {
2236
+ nextRunAt = now + ttlVal * 1000;
2237
+ this._cooldownBloom.add(key);
2238
+ this.log('info', `Restored cooldown for ${info.cmd}: ${ttlVal}s remaining`);
2239
+ }
2240
+ heap.push({ cmd: info.cmd, nextRunAt, priority: info.priority, info });
2185
2241
  }
2186
2242
  return heap;
2187
2243
  }
@@ -2309,7 +2365,7 @@ class AccountWorker {
2309
2365
 
2310
2366
  // Resume grind loop if it was running
2311
2367
  if (!this.commandQueue || this.commandQueue.size === 0) {
2312
- this.commandQueue = this.buildCommandQueue();
2368
+ this.commandQueue = await this.buildCommandQueue();
2313
2369
  }
2314
2370
  } else {
2315
2371
  this.log('error', 'Recovered connection but channel not found — retrying');
@@ -2407,7 +2463,7 @@ class AccountWorker {
2407
2463
  }
2408
2464
 
2409
2465
  if (!this.commandQueue || this.commandQueue.size === 0) {
2410
- this.commandQueue = this.buildCommandQueue();
2466
+ this.commandQueue = await this.buildCommandQueue();
2411
2467
  }
2412
2468
  if (!this.commandQueue || this.commandQueue.size === 0) {
2413
2469
  this.tickTimeout = setTimeout(() => this.tick(), 15000);
@@ -2558,22 +2614,33 @@ class AccountWorker {
2558
2614
 
2559
2615
  if (this.commandQueue && this.running && !shutdownCalled) {
2560
2616
  const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
2561
- let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
2562
2617
  this._lastCooldownOverride = null;
2563
2618
 
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);
2619
+ let scheduledWaitSec;
2620
+ if (hasOverride) {
2621
+ // Exact cooldown returned by command parser — use it without jitter or backoff.
2622
+ // This ensures work (1h), adventure (5h), stream (10m), farm (varies) are honored exactly.
2623
+ scheduledWaitSec = Math.max(1, this._lastCooldownOverride);
2624
+ } else {
2625
+ // Jitter-based cooldown for commands without a parsed override.
2626
+ let effectiveWait = totalWait;
2627
+
2628
+ // Smart fallback floors: static floor OR dynamically learned cooldown (whichever is higher).
2629
+ // Learned cooldowns come from actual parsed responses and persist across restarts via Redis.
2630
+ const staticFloor = AccountWorker.SMART_CD_FLOORS[item.cmd] || 0;
2631
+ const learnedCd = this._learnedCooldowns.get(item.cmd) || 0;
2632
+ const adaptiveFloor = Math.max(staticFloor, learnedCd);
2633
+ if (adaptiveFloor > 0) {
2634
+ effectiveWait = Math.max(effectiveWait, adaptiveFloor);
2569
2635
  }
2570
- }
2571
2636
 
2572
- if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
2573
- effectiveWait = MIN_FAIL_COOLDOWN;
2637
+ if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
2638
+ effectiveWait = MIN_FAIL_COOLDOWN;
2639
+ }
2640
+
2641
+ scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
2574
2642
  }
2575
2643
 
2576
- const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
2577
2644
  await this.setCooldown(item.cmd, scheduledWaitSec);
2578
2645
  item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
2579
2646
  this.commandQueue.push(item);
@@ -2611,7 +2678,8 @@ class AccountWorker {
2611
2678
  this.failStreak = 0;
2612
2679
  this.cycleCount = 0;
2613
2680
  this.lastCommandRun = 0;
2614
- this.commandQueue = this.buildCommandQueue();
2681
+ await this._loadLearnedCooldowns();
2682
+ this.commandQueue = await this.buildCommandQueue();
2615
2683
  this.lastHealthCheck = Date.now();
2616
2684
 
2617
2685
  // Reactive alert listener: run `pls alert` only when Dank Memer
@@ -3048,9 +3116,30 @@ async function start(apiKey, apiUrl) {
3048
3116
  }
3049
3117
  loginLines.push(` ${'─'.repeat(loginVis)}`);
3050
3118
  for (const l of loginLines) console.log(l);
3051
- loginLines = null;
3119
+
3120
+ // Dynamically capture the starting row of the login table via DSR
3121
+ let loginBaseRow = 1;
3122
+ const captureLoginRow = () => new Promise(resolve => {
3123
+ process.stdout.write(MARKER);
3124
+ const chunks = [];
3125
+ const handler = (chunk) => {
3126
+ chunks.push(chunk);
3127
+ const raw = chunks.join('');
3128
+ const m = raw.match(/\x1b\[(\d+);\d+R/);
3129
+ if (m) {
3130
+ process.stdin.removeListener('data', handler);
3131
+ loginBaseRow = parseInt(m[1], 10) + 1;
3132
+ resolve();
3133
+ }
3134
+ };
3135
+ process.stdin.on('data', handler);
3136
+ setTimeout(resolve, 50);
3137
+ });
3138
+ await captureLoginRow();
3052
3139
 
3053
3140
  let loginPending = new Array(accounts.length).fill(true);
3141
+ const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3142
+
3054
3143
  const drawLoginSpinners = () => {
3055
3144
  for (let i = 0; i < loginPending.length; i++) {
3056
3145
  if (!loginPending[i]) continue;
@@ -3059,8 +3148,13 @@ async function start(apiKey, apiUrl) {
3059
3148
  const name = loginStates[i].name.substring(0, colName).padEnd(colName);
3060
3149
  const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
3061
3150
  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`);
3151
+ const row = loginBaseRow + 1 + i; // +1 skips the top border line
3152
+ moveToRow(row);
3153
+ process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
3063
3154
  }
3155
+ // Move cursor back to bottom to avoid overwriting the bottom border
3156
+ const lastRow = loginBaseRow + 1 + accounts.length + 1;
3157
+ moveToRow(lastRow);
3064
3158
  };
3065
3159
  const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3066
3160
 
@@ -3089,7 +3183,9 @@ async function start(apiKey, apiUrl) {
3089
3183
  guild = 'timeout'.padEnd(colGuild);
3090
3184
  cmds = '···'.padEnd(colCmds);
3091
3185
  }
3092
- process.stdout.write(`\r\x1b[2K ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3186
+ const row = loginBaseRow + 1 + idx; // +1 skips the top border line
3187
+ moveToRow(row);
3188
+ process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3093
3189
  };
3094
3190
 
3095
3191
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
@@ -3137,6 +3233,28 @@ async function start(apiKey, apiUrl) {
3137
3233
  const iColVal = 16;
3138
3234
  const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3139
3235
 
3236
+ // Print a unique marker, query its position, then overwrite it with the table
3237
+ process.stdout.write(MARKER);
3238
+ let invBaseRow = 1;
3239
+ const captureRow = () => new Promise(resolve => {
3240
+ const chunks = [];
3241
+ const handler = (chunk) => {
3242
+ chunks.push(chunk);
3243
+ const raw = chunks.join('');
3244
+ const m = raw.match(/\x1b\[(\d+);\d+R/);
3245
+ if (m) {
3246
+ process.stdin.removeListener('data', handler);
3247
+ invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
3248
+ resolve();
3249
+ }
3250
+ };
3251
+ process.stdin.on('data', handler);
3252
+ setTimeout(resolve, 50);
3253
+ });
3254
+ await captureRow();
3255
+
3256
+ // Now print the inventory table starting at invBaseRow
3257
+ const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3140
3258
  console.log(` ${'─'.repeat(invVis)}`);
3141
3259
  for (let i = 0; i < activeWorkers.length; i++) {
3142
3260
  const w = activeWorkers[i];
@@ -3155,7 +3273,8 @@ async function start(apiKey, apiUrl) {
3155
3273
  const filled = Math.round(pct * barW);
3156
3274
  const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3157
3275
  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} `);
3276
+ invMoveToRow(invBaseRow);
3277
+ 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
3278
  };
3160
3279
  const invSpinnerInterval = setInterval(drawInvProgress, 80);
3161
3280
 
@@ -3171,7 +3290,9 @@ async function start(apiKey, apiUrl) {
3171
3290
  const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3172
3291
  const itemStr = `${items}`.padEnd(iColItems);
3173
3292
  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`);
3293
+ const row = invBaseRow + 1 + i;
3294
+ invMoveToRow(row);
3295
+ process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3175
3296
  if (invRes?.ok) invDone++; else invFailed++;
3176
3297
  }));
3177
3298
 
@@ -3195,6 +3316,27 @@ async function start(apiKey, apiUrl) {
3195
3316
  const bColLs = 4;
3196
3317
  const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3197
3318
 
3319
+ // Capture starting row for balance phase
3320
+ process.stdout.write(MARKER);
3321
+ let balBaseRow = 1;
3322
+ const balCaptureRow = () => new Promise(resolve => {
3323
+ const chunks = [];
3324
+ const handler = (chunk) => {
3325
+ chunks.push(chunk);
3326
+ const raw = chunks.join('');
3327
+ const m = raw.match(/\x1b\[(\d+);\d+R/);
3328
+ if (m) {
3329
+ process.stdin.removeListener('data', handler);
3330
+ balBaseRow = parseInt(m[1], 10) + 1;
3331
+ resolve();
3332
+ }
3333
+ };
3334
+ process.stdin.on('data', handler);
3335
+ setTimeout(resolve, 50);
3336
+ });
3337
+ await balCaptureRow();
3338
+
3339
+ const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3198
3340
  console.log(` ${'─'.repeat(balVis)}`);
3199
3341
  for (let i = 0; i < activeWorkers.length; i++) {
3200
3342
  const w = activeWorkers[i];
@@ -3212,7 +3354,8 @@ async function start(apiKey, apiUrl) {
3212
3354
  const barW = Math.min(20, startupTw - 40);
3213
3355
  const filled = Math.round(pct * barW);
3214
3356
  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} `);
3357
+ balMoveToRow(balBaseRow);
3358
+ 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
3359
  };
3217
3360
  const balSpinnerInterval = setInterval(drawBalProgress, 80);
3218
3361
 
@@ -3228,7 +3371,9 @@ async function start(apiKey, apiUrl) {
3228
3371
  const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3229
3372
  const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3230
3373
  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`);
3374
+ const row = balBaseRow + 1 + i;
3375
+ balMoveToRow(row);
3376
+ 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
3377
  balDone++;
3233
3378
  }));
3234
3379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.34.0",
3
+ "version": "6.39.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"