dankgrinder 6.42.0 → 6.46.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.
@@ -198,7 +198,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
198
198
  LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
199
199
 
200
200
  await channel.send('pls inv');
201
- let response = await waitForDankMemer(10000);
201
+ let response = await waitForDankMemer(15000);
202
202
 
203
203
  if (!response) {
204
204
  LOG.warn('[inv] No response');
package/lib/grinder.js CHANGED
@@ -2026,16 +2026,6 @@ class AccountWorker {
2026
2026
  return;
2027
2027
  }
2028
2028
 
2029
- // Died flag from crime/search handler (death detected in the command response)
2030
- if (cmdResult.died) {
2031
- this.log('error', `${cmdName} → DIED! Checking lifesaver count...`);
2032
- // The DM will come separately with the actual death details
2033
- // For now, be cautious — set a short cooldown and let DM listener handle the rest
2034
- await this.setCooldown('crime', 300); // 5 min cooldown to check DMs
2035
- await this.setCooldown('search', 300);
2036
- return;
2037
- }
2038
-
2039
2029
  // Premium-only command detection — disable permanently
2040
2030
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
2041
2031
  resultLower.includes('buy the ability to use this command') ||
@@ -2625,6 +2615,18 @@ class AccountWorker {
2625
2615
  return;
2626
2616
  }
2627
2617
 
2618
+ // Startup delay: don't send commands for the first 30s after grindLoop() starts.
2619
+ // This prevents flooding Dank Memer during the Phase 2 inventory check which
2620
+ // sends pls inv for all accounts simultaneously after login.
2621
+ if (this._startupDelayUntil && now < this._startupDelayUntil) {
2622
+ const waitMs = this._startupDelayUntil - now;
2623
+ this.setStatus('warming up...');
2624
+ item.nextRunAt = now + waitMs + 1000;
2625
+ if (this.commandQueue) this.commandQueue.push(item);
2626
+ this.tickTimeout = setTimeout(() => this.tick(), waitMs + 1000);
2627
+ return;
2628
+ }
2629
+
2628
2630
  this.busy = true;
2629
2631
 
2630
2632
  // ── Run command (with interactive retry) ───────────────────
@@ -2765,6 +2767,11 @@ class AccountWorker {
2765
2767
  this.failStreak = 0;
2766
2768
  this.cycleCount = 0;
2767
2769
  this.lastCommandRun = 0;
2770
+ // Delay first command by 30s to avoid competing with Phase 2 inventory check
2771
+ // which sends pls inv for all accounts simultaneously after login.
2772
+ // Without this, the grind loop floods Dank Memer with commands during the
2773
+ // login surge, triggering rate-limits that cause Phase 2 inventory to fail.
2774
+ this._startupDelayUntil = Date.now() + 30000;
2768
2775
  await this._loadLearnedCooldowns();
2769
2776
  this.commandQueue = await this.buildCommandQueue();
2770
2777
  this.lastHealthCheck = Date.now();
@@ -3165,19 +3172,38 @@ async function start(apiKey, apiUrl) {
3165
3172
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
3166
3173
  if (REDIS_URL) {
3167
3174
  rawLogger.init(REDIS_URL).catch(() => {});
3168
- // Listen for DM death events across all accounts
3175
+ // Live DM listener: detect deaths and level-ups in real-time across all accounts
3169
3176
  rawLogger.onDmEvent((event, raw) => {
3170
- if (event.type === 'death' && event.lifesaversLeft === 0) {
3171
- const channelId = raw.channel_id;
3172
- // Find which worker uses this DM channel and disable their crime/search
3173
- for (const w of workers) {
3174
- if (w.client?.user?.dmChannel?.id === channelId || w.channel?.id) {
3175
- w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
3176
- w.setCooldown?.('crime', 86400);
3177
- w.setCooldown?.('search', 86400);
3177
+ const dmChannelId = raw.channel_id;
3178
+
3179
+ // Find which worker owns this DM channel
3180
+ const worker = workers.find(w => w._dmChannelId === dmChannelId);
3181
+ if (!worker) return;
3182
+
3183
+ if (event.type === 'death') {
3184
+ const lsLeft = event.lifesaversLeft;
3185
+
3186
+ if (lsLeft === 0) {
3187
+ // 0 lifesavers — disable crime/search immediately
3188
+ worker.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
3189
+ worker.setCooldown?.('crime', 86400);
3190
+ worker.setCooldown?.('search', 86400);
3191
+ worker._lifesavers = 0;
3192
+ sendWebhook?.('DEATH ALERT (DM)', `**${worker.username}** died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
3193
+ } else if (lsLeft > 0) {
3194
+ // Lifesaver(s) used — update count in real-time
3195
+ worker._lifesavers = lsLeft;
3196
+ worker.log?.('warn', `Lifesaver used! ${lsLeft} remaining.`);
3197
+ if (lsLeft <= 2) {
3198
+ sendWebhook?.('LOW LIFESAVERS', `**${worker.username}** died! Only **${lsLeft}** lifesaver(s) left!`, 0xfbbf24);
3178
3199
  }
3179
3200
  }
3180
- sendWebhook?.('DEATH ALERT (DM)', `Account died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
3201
+ } else if (event.type === 'levelup') {
3202
+ // Level up — update in-memory level
3203
+ if (event.to > 0) {
3204
+ worker._level = event.to;
3205
+ worker.log?.('info', `Level up! Now level ${event.to}.`);
3206
+ }
3181
3207
  }
3182
3208
  });
3183
3209
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
@@ -3191,39 +3217,21 @@ async function start(apiKey, apiUrl) {
3191
3217
  console.log(` ${checks.join(' ')}`);
3192
3218
  console.log('');
3193
3219
 
3194
- // ── Phase 1: Login with per-account inline rendering ─────────────────────────
3220
+ // ── Phase 1: Login — inline table with per-row updates ─────────
3195
3221
  const startupTw = process.stdout.columns || 90;
3196
- const colNum = 4; // " #"
3197
- const colSts = 3; // "ST"
3222
+ const colNum = 4;
3223
+ const colSts = 3;
3198
3224
  const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
3199
3225
  const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
3200
3226
  const colCmds = 8;
3201
3227
  const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
3202
3228
 
3203
- const loginStates = accounts.map((acc, i) => ({
3204
- name: acc.label || acc.id || '?',
3205
- done: false,
3206
- failed: false,
3207
- worker: null,
3208
- }));
3209
-
3210
- let loginLines = [];
3211
- loginLines.push(` ${'─'.repeat(loginVis)}`);
3212
- for (let i = 0; i < loginStates.length; i++) {
3213
- const s = loginStates[i];
3214
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3215
- const name = s.name.substring(0, colName).padEnd(colName);
3216
- const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
3217
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3218
- loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
3219
- }
3220
- loginLines.push(` ${'─'.repeat(loginVis)}`);
3221
- for (const l of loginLines) console.log(l);
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);
3229
+ // Use DSR to find starting row, then use explicit row numbers for all table writes.
3230
+ // This avoids relying on cursor tracking via \n which varies by terminal.
3231
+ let tableTopRow = 1;
3232
+ let pendingSet = new Set(Array.from({ length: accounts.length }, (_, i) => i));
3233
+ const captureTopRow = () => new Promise(resolve => {
3234
+ process.stdout.write('\x1b[6n');
3227
3235
  const chunks = [];
3228
3236
  const handler = (chunk) => {
3229
3237
  chunks.push(chunk);
@@ -3231,51 +3239,52 @@ async function start(apiKey, apiUrl) {
3231
3239
  const m = raw.match(/\x1b\[(\d+);\d+R/);
3232
3240
  if (m) {
3233
3241
  process.stdin.removeListener('data', handler);
3234
- loginBaseRow = parseInt(m[1], 10) + 1;
3242
+ tableTopRow = parseInt(m[1], 10);
3235
3243
  resolve();
3236
3244
  }
3237
3245
  };
3238
3246
  process.stdin.on('data', handler);
3239
3247
  setTimeout(resolve, 50);
3240
3248
  });
3241
- await captureLoginRow();
3242
-
3243
- let loginPending = new Array(accounts.length).fill(true);
3244
- const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3249
+ await captureTopRow();
3250
+
3251
+ // Absolute row numbers for all table elements (calculated from captured top row)
3252
+ const borderTopRow = tableTopRow; // border
3253
+ const dataStartRow = tableTopRow + 1; // first account row
3254
+ const borderBotRow = tableTopRow + accounts.length + 1; // bottom border
3255
+ const bottomRow = borderBotRow + 1; // cursor final position after table
3256
+
3257
+ // Print initial table using explicit row positioning
3258
+ process.stdout.write(`\x1b[${borderTopRow};1H ${'─'.repeat(loginVis)}`);
3259
+ for (let i = 0; i < accounts.length; i++) {
3260
+ const row = dataStartRow + i;
3261
+ const name = (accounts[i].label || accounts[i].id || '?').substring(0, colName).padEnd(colName);
3262
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3263
+ process.stdout.write(`\x1b[${row};1H ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'···'.padEnd(colGuild)}${c.reset} ${c.dim}${'···'.padEnd(colCmds)}${c.reset}\x1b[K`);
3264
+ }
3265
+ process.stdout.write(`\x1b[${borderBotRow};1H ${'─'.repeat(loginVis)}\x1b[K`);
3245
3266
 
3267
+ // Spinner: updates rows inline using absolute row numbers
3246
3268
  const drawLoginSpinners = () => {
3247
- for (let i = 0; i < loginPending.length; i++) {
3248
- if (!loginPending[i]) continue;
3269
+ for (const i of pendingSet) {
3249
3270
  const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3271
+ const name = (accounts[i].label || accounts[i].id || '?').substring(0, colName).padEnd(colName);
3250
3272
  const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3251
- const name = loginStates[i].name.substring(0, colName).padEnd(colName);
3252
- const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
3253
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
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`);
3257
- }
3258
- // Move cursor back to bottom to avoid overwriting the bottom border
3259
- const lastRow = loginBaseRow + 1 + accounts.length + 1;
3260
- moveToRow(lastRow);
3273
+ process.stdout.write(`\x1b[${dataStartRow + i};1H ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${c.dim}${'logging in...'.substring(0, colGuild)}${c.reset} ${c.dim}${'···'.padEnd(colCmds)}${c.reset}\x1b[K`);
3274
+ }
3261
3275
  };
3262
- const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3263
-
3264
- const finalizeLoginLine = (idx, worker) => {
3265
- if (!loginPending[idx]) return;
3266
- loginPending[idx] = false;
3267
- const s = loginStates[idx];
3268
- s.done = true;
3269
- s.worker = worker;
3276
+ const loginSpinner = setInterval(drawLoginSpinners, 80);
3270
3277
 
3278
+ const finalizeLoginRow = (idx, worker) => {
3279
+ if (!pendingSet.has(idx)) return;
3280
+ pendingSet.delete(idx);
3271
3281
  const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
3272
- const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3282
+ const name = (worker.username || accounts[idx].label || accounts[idx].id || '?').substring(0, colName).padEnd(colName);
3273
3283
  let sts, guild, cmds;
3274
3284
  if (worker._tokenInvalid) {
3275
3285
  sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3276
3286
  guild = 'INVALID'.padEnd(colGuild);
3277
3287
  cmds = '···'.padEnd(colCmds);
3278
- s.failed = true;
3279
3288
  } else if (worker.channel) {
3280
3289
  sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3281
3290
  const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
@@ -3286,9 +3295,7 @@ async function start(apiKey, apiUrl) {
3286
3295
  guild = 'timeout'.padEnd(colGuild);
3287
3296
  cmds = '···'.padEnd(colCmds);
3288
3297
  }
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`);
3298
+ process.stdout.write(`\x1b[${dataStartRow + idx};1H ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3292
3299
  };
3293
3300
 
3294
3301
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
@@ -3306,58 +3313,34 @@ async function start(apiKey, apiUrl) {
3306
3313
  const worker = new AccountWorker(acc, i + idx);
3307
3314
  workers.push(worker);
3308
3315
  workerMap.set(acc.id, worker);
3309
- loginStates[i + idx].worker = worker;
3310
3316
  await worker.start();
3311
- finalizeLoginLine(i + idx, worker);
3317
+ finalizeLoginRow(i + idx, worker);
3312
3318
  }));
3313
3319
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3314
3320
  hintGC();
3315
3321
  }
3316
3322
 
3317
- clearInterval(loginSpinnerInterval);
3318
- const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3323
+ clearInterval(loginSpinner);
3319
3324
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3320
3325
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3321
- console.log(`\r\x1b[2K ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3322
- console.log('');
3326
+ const activeWorkers = workers.filter(w => !w._tokenInvalid);
3327
+ const loginDone = activeWorkers.filter(w => w.channel).length;
3328
+ // Clear bottom border row, move to new line, print login complete
3329
+ process.stdout.write(`\x1b[${borderBotRow};1H\x1b[2K\n ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}\n`);
3323
3330
  if (invalidWorkers.length > 0) {
3324
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3325
- for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3326
- console.log('');
3331
+ console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}${c.red}${invalidWorkers.length} INVALID token(s):${c.reset} ${invalidWorkers.map(w => w.account.label || w.account.id).join(', ')}`);
3327
3332
  }
3328
3333
  if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3334
+ console.log('');
3329
3335
 
3330
- const activeWorkers = workers.filter(w => !w._tokenInvalid);
3336
+ // ── Phase 2: Inventory check — clean sequential table ─────────
3331
3337
 
3332
- // ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
3333
3338
  const iColNum = 4;
3334
3339
  const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3335
3340
  const iColItems = 8;
3336
3341
  const iColVal = 16;
3337
3342
  const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3338
3343
 
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`);
3361
3344
  console.log(` ${'─'.repeat(invVis)}`);
3362
3345
  for (let i = 0; i < activeWorkers.length; i++) {
3363
3346
  const w = activeWorkers[i];
@@ -3367,40 +3350,31 @@ async function start(apiKey, apiUrl) {
3367
3350
  }
3368
3351
  console.log(` ${'─'.repeat(invVis)}`);
3369
3352
 
3370
- let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3371
- const drawInvProgress = () => {
3372
- if (invPending === 0) return;
3373
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3374
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3375
- const barW = Math.min(20, startupTw - 40);
3376
- const filled = Math.round(pct * barW);
3377
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3378
- const pctStr = `${Math.round(pct * 100)}%`;
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`);
3381
- };
3382
- const invSpinnerInterval = setInterval(drawInvProgress, 80);
3353
+ const invResults = await Promise.all(activeWorkers.map(async (w, i) => {
3354
+ try {
3355
+ return await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 5, silent: true });
3356
+ } catch {
3357
+ return { ok: false };
3358
+ }
3359
+ }));
3383
3360
 
3384
- await Promise.all(activeWorkers.map(async (w, i) => {
3361
+ let invDone = 0, invFailed = 0;
3362
+ // Re-print table with results
3363
+ console.log(` ${'─'.repeat(invVis)}`);
3364
+ for (let i = 0; i < activeWorkers.length; i++) {
3365
+ const invRes = invResults[i] || { ok: false };
3366
+ const w = activeWorkers[i];
3385
3367
  const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3386
3368
  const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3387
- let invRes;
3388
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3389
- catch { invRes = { ok: false }; }
3390
- invPending--;
3391
3369
  const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3392
3370
  const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3393
3371
  const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3394
3372
  const itemStr = `${items}`.padEnd(iColItems);
3395
3373
  const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3396
- const row = invBaseRow + 1 + i;
3397
- invMoveToRow(row);
3398
- process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3374
+ console.log(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}`);
3399
3375
  if (invRes?.ok) invDone++; else invFailed++;
3400
- }));
3401
-
3402
- clearInterval(invSpinnerInterval);
3403
- process.stdout.write(`\r\x1b[2K`);
3376
+ }
3377
+ console.log(` ${'─'.repeat(invVis)}`);
3404
3378
 
3405
3379
  if (invFailed > 0) {
3406
3380
  console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}Inventory incomplete${c.reset} ${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
@@ -3410,7 +3384,7 @@ async function start(apiKey, apiUrl) {
3410
3384
  console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${activeWorkers.length}${c.reset} ${c.dim}all clear${c.reset}`);
3411
3385
  console.log('');
3412
3386
 
3413
- // ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
3387
+ // ── Phase 2.5: Balance check — clean sequential table ─────────
3414
3388
  const bColNum = 4;
3415
3389
  const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3416
3390
  const bColWallet = 12;
@@ -3419,27 +3393,6 @@ async function start(apiKey, apiUrl) {
3419
3393
  const bColLs = 4;
3420
3394
  const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3421
3395
 
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`);
3443
3396
  console.log(` ${'─'.repeat(balVis)}`);
3444
3397
  for (let i = 0; i < activeWorkers.length; i++) {
3445
3398
  const w = activeWorkers[i];
@@ -3449,22 +3402,14 @@ async function start(apiKey, apiUrl) {
3449
3402
  }
3450
3403
  console.log(` ${'─'.repeat(balVis)}`);
3451
3404
 
3452
- let balDone = 0, balPending = activeWorkers.length;
3453
- const drawBalProgress = () => {
3454
- if (balPending === 0) return;
3455
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3456
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3457
- const barW = Math.min(20, startupTw - 40);
3458
- const filled = Math.round(pct * barW);
3459
- const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + 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`);
3462
- };
3463
- const balSpinnerInterval = setInterval(drawBalProgress, 80);
3464
-
3465
- await Promise.all(activeWorkers.map(async (w, i) => {
3405
+ await Promise.all(activeWorkers.map(async (w) => {
3466
3406
  try { await w.checkBalance(true); } catch {}
3467
- balPending--;
3407
+ }));
3408
+
3409
+ // Re-print table with results
3410
+ console.log(` ${'─'.repeat(balVis)}`);
3411
+ for (let i = 0; i < activeWorkers.length; i++) {
3412
+ const w = activeWorkers[i];
3468
3413
  const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3469
3414
  const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3470
3415
  const wallet = w.stats?.balance || 0;
@@ -3474,14 +3419,9 @@ async function start(apiKey, apiUrl) {
3474
3419
  const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3475
3420
  const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3476
3421
  const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
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`);
3480
- balDone++;
3481
- }));
3482
-
3483
- clearInterval(balSpinnerInterval);
3484
- process.stdout.write(`\r\x1b[2K`);
3422
+ console.log(` ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}`);
3423
+ }
3424
+ console.log(` ${'─'.repeat(balVis)}`);
3485
3425
 
3486
3426
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3487
3427
  for (const w of activeWorkers) {
@@ -3495,7 +3435,6 @@ async function start(apiKey, apiUrl) {
3495
3435
  }
3496
3436
  console.log('');
3497
3437
 
3498
-
3499
3438
  // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3500
3439
  console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3501
3440
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.42.0",
3
+ "version": "6.46.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"