dankgrinder 7.72.0 → 7.74.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.
Files changed (2) hide show
  1. package/lib/grinder.js +48 -316
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -94,10 +94,20 @@ 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@@';
99
97
  const DANK_MEMER_ID = '270904126974590976';
100
98
 
99
+
100
+ // Simple uptime formatter
101
+ function formatUptime() {
102
+ const s = Math.floor((Date.now() - startTime) / 1000);
103
+ if (s < 60) return `${s}s`;
104
+ const m = Math.floor(s / 60);
105
+ if (m < 60) return `${m}m ${s % 60}s`;
106
+ const h = Math.floor(m / 60);
107
+ if (h < 24) return `${h}h ${m % 60}m`;
108
+ const d = Math.floor(h / 24);
109
+ return `${d}d ${h % 24}h`;
110
+ }
101
111
  // ── Safe options for search/crime ──────────────────────────
102
112
  // Object.freeze → V8 marks these as immutable, enabling inline caching
103
113
  // and preventing accidental mutation across 10K worker instances.
@@ -272,17 +282,6 @@ function progressBar(value, max, width, filledColor, emptyColor) {
272
282
  return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
273
283
  }
274
284
 
275
- // ── Animated braille spinner frames ──────────────────────────
276
- const BRAILLE_SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
277
- const BLOCK_SPIN = ['▉', '▊', '▋', '▌', '▍', '▎', '▏', '▎', '▍', '▌', '▋', '▊'];
278
- const PULSE_CHARS = ['○', '◎', '●', '◉', '●', '◎'];
279
- function getSpinner(type = 'braille') {
280
- const now = Math.floor(Date.now() / 80);
281
- if (type === 'block') return BLOCK_SPIN[now % BLOCK_SPIN.length];
282
- if (type === 'pulse') return PULSE_CHARS[now % PULSE_CHARS.length];
283
- return BRAILLE_SPIN[now % BRAILLE_SPIN.length];
284
- }
285
-
286
285
  // ── Box drawing helpers ──────────────────────────────────────
287
286
  const BOX = {
288
287
  tl: '╭', tr: '╮', bl: '╰', br: '╯',
@@ -1389,9 +1388,7 @@ class AccountWorker {
1389
1388
 
1390
1389
  // Update Redis with findings
1391
1390
  if (redis) {
1392
- if (currentLevel > 0) {
1393
- await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1394
- this._level = currentLevel;
1391
+ try {
1395
1392
  if (currentLevel > 0) {
1396
1393
  await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1397
1394
  this._level = currentLevel;
@@ -1404,17 +1401,19 @@ class AccountWorker {
1404
1401
  await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1405
1402
  }
1406
1403
  }
1407
- }
1408
- return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1409
- } catch (e) {
1410
- lastError = e;
1411
- if (attempt < maxRetries - 1) {
1412
- await new Promise(r => setTimeout(r, delays[attempt]));
1413
- }
1404
+ } catch { /* Redis errors non-fatal */ }
1405
+ }
1406
+
1407
+ return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1408
+ } catch (e) {
1409
+ lastError = e;
1410
+ if (attempt < maxRetries - 1) {
1411
+ await new Promise(r => setTimeout(r, delays[attempt]));
1414
1412
  }
1415
1413
  }
1416
- return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1417
1414
  }
1415
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1416
+ }
1418
1417
 
1419
1418
  // ── Run Single Command ──────────────────────────────────────
1420
1419
  // Each modular command handler sends the command, waits for response,
@@ -2749,10 +2748,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2749
2748
  REDIS_URL = process.env.REDIS_URL || '';
2750
2749
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2751
2750
 
2752
- process.stdout.write('\x1b[2J\x1b[H');
2753
- const tw = Math.min(process.stdout.columns || 80, 78);
2754
- const bar = c.dim + '─'.repeat(tw) + c.reset;
2755
-
2756
2751
  // Detect zlib-sync availability
2757
2752
  let hasZlib = false;
2758
2753
  try { require('zlib-sync'); hasZlib = true; } catch {}
@@ -2880,110 +2875,8 @@ async function start(apiKey, apiUrl, opts = {}) {
2880
2875
  console.log(` ${checks.join(' ')}`);
2881
2876
  console.log('');
2882
2877
 
2883
- // ── Phase 1: Login with per-account inline rendering ─────────────────────────
2884
- const startupTw = process.stdout.columns || 90;
2885
- const colNum = 4; // " #"
2886
- const colSts = 3; // "ST"
2887
- const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
2888
- const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
2889
- const colCmds = 8;
2890
- const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
2891
-
2892
- const loginStates = accounts.map((acc, i) => ({
2893
- name: acc.label || acc.id || '?',
2894
- done: false,
2895
- failed: false,
2896
- worker: null,
2897
- }));
2898
-
2899
- let loginLines = [];
2900
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2901
- for (let i = 0; i < loginStates.length; i++) {
2902
- const s = loginStates[i];
2903
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
2904
- const name = s.name.substring(0, colName).padEnd(colName);
2905
- const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
2906
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
2907
- loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
2908
- }
2909
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2910
- for (const l of loginLines) console.log(l);
2911
-
2912
- // Dynamically capture the starting row of the login table via DSR.
2913
- // Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
2914
- // of the visible "@MARKER@@" text portion, which was causing the DSR
2915
- // response to be swallowed or delayed.
2916
- let loginBaseRow = 1;
2917
- const captureLoginRow = () => new Promise(resolve => {
2918
- const chunks = [];
2919
- const handler = (chunk) => {
2920
- chunks.push(chunk);
2921
- const raw = chunks.join('');
2922
- const m = raw.match(/\x1b\[(\d+);\d+R/);
2923
- if (m) {
2924
- process.stdin.removeListener('data', handler);
2925
- loginBaseRow = parseInt(m[1], 10) + 1;
2926
- resolve();
2927
- }
2928
- };
2929
- process.stdin.on('data', handler);
2930
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
2931
- process.stderr.write(MARKER);
2932
- setTimeout(resolve, 50);
2933
- });
2934
- await captureLoginRow();
2935
-
2936
- let loginPending = new Array(accounts.length).fill(true);
2937
- const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
2938
-
2939
- const drawLoginSpinners = () => {
2940
- for (let i = 0; i < loginPending.length; i++) {
2941
- if (!loginPending[i]) continue;
2942
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
2943
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
2944
- const name = loginStates[i].name.substring(0, colName).padEnd(colName);
2945
- const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
2946
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
2947
- const row = loginBaseRow + 1 + i; // +1 skips the top border line
2948
- moveToRow(row);
2949
- process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
2950
- }
2951
- // Move cursor back to bottom to avoid overwriting the bottom border
2952
- const lastRow = loginBaseRow + 1 + accounts.length + 1;
2953
- moveToRow(lastRow);
2954
- };
2955
- const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
2956
-
2957
- const finalizeLoginLine = (idx, worker) => {
2958
- if (!loginPending[idx]) return;
2959
- loginPending[idx] = false;
2960
- const s = loginStates[idx];
2961
- s.done = true;
2962
- s.worker = worker;
2963
-
2964
- const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
2965
- const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
2966
- let sts, guild, cmds;
2967
- if (worker._tokenInvalid) {
2968
- sts = `${rgb(239, 68, 68)}✗${c.reset}`;
2969
- guild = 'INVALID'.padEnd(colGuild);
2970
- cmds = '···'.padEnd(colCmds);
2971
- s.failed = true;
2972
- } else if (worker.channel) {
2973
- sts = `${rgb(52, 211, 153)}✓${c.reset}`;
2974
- const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
2975
- guild = gn.padEnd(colGuild);
2976
- cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
2977
- } else {
2978
- sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
2979
- guild = 'timeout'.padEnd(colGuild);
2980
- cmds = '···'.padEnd(colCmds);
2981
- }
2982
- const row = loginBaseRow + 1 + idx; // +1 skips the top border line
2983
- moveToRow(row);
2984
- process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
2985
- };
2986
-
2878
+ // ── Phase 1: Login accounts ─────────────────────────────────────────
2879
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
2987
2880
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2988
2881
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2989
2882
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
@@ -2999,203 +2892,58 @@ async function start(apiKey, apiUrl, opts = {}) {
2999
2892
  const worker = new AccountWorker(acc, i + idx);
3000
2893
  workers.push(worker);
3001
2894
  workerMap.set(acc.id, worker);
3002
- loginStates[i + idx].worker = worker;
3003
2895
  await worker.start();
3004
- finalizeLoginLine(i + idx, worker);
3005
2896
  }));
3006
2897
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3007
2898
  hintGC();
3008
2899
  }
3009
2900
 
3010
- clearInterval(loginSpinnerInterval);
3011
2901
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3012
2902
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3013
2903
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3014
- 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}`);
3015
- console.log('');
2904
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${loginDone}/${accounts.length} accounts connected`);
3016
2905
  if (invalidWorkers.length > 0) {
3017
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3018
- for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token is invalid or expired`);
3019
- console.log('');
2906
+ log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
2907
+ for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
3020
2908
  }
3021
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2909
+ if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login`);
3022
2910
 
3023
2911
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3024
2912
 
3025
- // ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
3026
- const iColNum = 4;
3027
- const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3028
- const iColItems = 8;
3029
- const iColVal = 16;
3030
- const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3031
-
3032
- // Print a unique marker, query its position, then overwrite it with the table
3033
- // Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
3034
- let invBaseRow = 1;
3035
- const captureRow = () => new Promise(resolve => {
3036
- const chunks = [];
3037
- const handler = (chunk) => {
3038
- chunks.push(chunk);
3039
- const raw = chunks.join('');
3040
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3041
- if (m) {
3042
- process.stdin.removeListener('data', handler);
3043
- invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
3044
- resolve();
3045
- }
3046
- };
3047
- process.stdin.on('data', handler);
3048
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3049
- process.stderr.write(MARKER);
3050
- setTimeout(resolve, 50);
3051
- });
3052
- await captureRow();
3053
-
3054
- // Now print the inventory table starting at invBaseRow
3055
- const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3056
- console.log(` ${'─'.repeat(invVis)}`);
3057
- for (let i = 0; i < activeWorkers.length; i++) {
3058
- const w = activeWorkers[i];
3059
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3060
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3061
- console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'···'.padEnd(iColVal)}${c.reset}`);
3062
- }
3063
- console.log(` ${'─'.repeat(invVis)}`);
3064
-
3065
- let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3066
- const drawInvProgress = () => {
3067
- if (invPending === 0) return;
3068
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3069
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3070
- const barW = Math.min(20, startupTw - 40);
3071
- const filled = Math.round(pct * barW);
3072
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3073
- const pctStr = `${Math.round(pct * 100)}%`;
3074
- invMoveToRow(invBaseRow);
3075
- 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`);
3076
- };
3077
- const invSpinnerInterval = setInterval(drawInvProgress, 80);
3078
-
3079
- await Promise.all(activeWorkers.map(async (w, i) => {
3080
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3081
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3082
- let invRes;
3083
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3084
- catch { invRes = { ok: false }; }
3085
- invPending--;
3086
- const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3087
- const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3088
- const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3089
- const itemStr = `${items}`.padEnd(iColItems);
3090
- const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3091
- const row = invBaseRow + 1 + i;
3092
- invMoveToRow(row);
3093
- process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3094
- if (invRes?.ok) invDone++; else invFailed++;
2913
+ // ── Phase 2: Inventory check ─────────────────────────────────────
2914
+ console.log(` ${rgb(34, 211, 238)}·${c.reset} Checking inventory (${activeWorkers.length} accounts)...`);
2915
+ let invDone = 0, invFailed = 0;
2916
+ await Promise.all(activeWorkers.map(async (w) => {
2917
+ try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2918
+ catch { invFailed++; return; }
2919
+ invDone++;
3095
2920
  }));
3096
2921
 
3097
- clearInterval(invSpinnerInterval);
3098
- process.stdout.write(`\r\x1b[2K`);
3099
-
3100
2922
  if (invFailed > 0) {
3101
- 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}`);
3102
- log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
2923
+ console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed not starting grind loops`);
3103
2924
  return;
3104
2925
  }
3105
- 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}`);
3106
- console.log('');
2926
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Inventory: ${invDone}/${activeWorkers.length} clear`);
3107
2927
 
3108
- // ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
3109
- const bColNum = 4;
3110
- const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3111
- const bColWallet = 12;
3112
- const bColBank = 12;
3113
- const bColTotal = 14;
3114
- const bColLs = 4;
3115
- const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3116
-
3117
- // Capture starting row for balance phase
3118
- // Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
3119
- let balBaseRow = 1;
3120
- const balCaptureRow = () => new Promise(resolve => {
3121
- const chunks = [];
3122
- const handler = (chunk) => {
3123
- chunks.push(chunk);
3124
- const raw = chunks.join('');
3125
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3126
- if (m) {
3127
- process.stdin.removeListener('data', handler);
3128
- balBaseRow = parseInt(m[1], 10) + 1;
3129
- resolve();
3130
- }
3131
- };
3132
- process.stdin.on('data', handler);
3133
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3134
- process.stderr.write(MARKER);
3135
- setTimeout(resolve, 50);
3136
- });
3137
- await balCaptureRow();
3138
-
3139
- const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3140
- console.log(` ${'─'.repeat(balVis)}`);
3141
- for (let i = 0; i < activeWorkers.length; i++) {
3142
- const w = activeWorkers[i];
3143
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3144
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3145
- console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'···'.padEnd(bColBank)}${c.reset} ${c.dim}${'···'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3146
- }
3147
- console.log(` ${'─'.repeat(balVis)}`);
3148
-
3149
- let balDone = 0, balPending = activeWorkers.length;
3150
- const drawBalProgress = () => {
3151
- if (balPending === 0) return;
3152
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3153
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3154
- const barW = Math.min(20, startupTw - 40);
3155
- const filled = Math.round(pct * barW);
3156
- const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3157
- balMoveToRow(balBaseRow);
3158
- 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`);
3159
- };
3160
- const balSpinnerInterval = setInterval(drawBalProgress, 80);
3161
-
3162
- await Promise.all(activeWorkers.map(async (w, i) => {
2928
+ // ── Phase 2.5: Balance check ────────────────────────────────────
2929
+ console.log(` ${rgb(251, 191, 36)}·${c.reset} Checking balance (${activeWorkers.length} accounts)...`);
2930
+ await Promise.all(activeWorkers.map(async (w) => {
3163
2931
  try { await w.checkBalance(true); } catch {}
3164
- balPending--;
3165
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3166
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3167
- const wallet = w.stats?.balance || 0;
3168
- const bank = w.stats?.bankBalance || 0;
3169
- const ls = w._lifesavers ?? '?';
3170
- const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3171
- const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3172
- const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3173
- const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3174
- const row = balBaseRow + 1 + i;
3175
- balMoveToRow(row);
3176
- 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`);
3177
- balDone++;
3178
2932
  }));
3179
2933
 
3180
- clearInterval(balSpinnerInterval);
3181
- process.stdout.write(`\r\x1b[2K`);
3182
-
3183
2934
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3184
2935
  for (const w of activeWorkers) {
3185
2936
  totalWallet += w.stats?.balance || 0;
3186
2937
  totalBank += w.stats?.bankBalance || 0;
3187
2938
  if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
3188
2939
  }
3189
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Balance${c.reset} Total: ${c.bold}${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
2940
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Balance: ${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
3190
2941
  if (noLifesaverAccounts.length > 0) {
3191
- console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}WARNING: ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS!${c.reset} Crime/Search disabled for: ${noLifesaverAccounts.join(', ')}`);
2942
+ console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
3192
2943
  }
3193
- console.log('');
3194
2944
 
3195
-
3196
- // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3197
- const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3198
- console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
2945
+ // ── Phase 2.75: DM history check ────────────────────────────────
2946
+ console.log(` ${rgb(139, 92, 246)}·${c.reset} Checking DM history...`);
3199
2947
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
3200
2948
  for (const w of activeWorkers) {
3201
2949
  try {
@@ -3204,25 +2952,12 @@ async function start(apiKey, apiUrl, opts = {}) {
3204
2952
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3205
2953
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
3206
2954
  if (dm.lifesavers === -1) dmUnknown.push(w.username);
3207
- // Store level and lifesaver for dashboard
3208
2955
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
3209
2956
  if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
3210
- const parts = [];
3211
- if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
3212
- if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
3213
- if (dm.lifesavers >= 0) {
3214
- const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3215
- parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
3216
- } else {
3217
- // Unknown lifesavers — pulse to show pending
3218
- const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3219
- parts.push(`${D}${pulse}♥?${c.reset}`);
3220
- }
3221
2957
  } catch {}
3222
2958
  }
3223
2959
  if (dmNoLs.length > 0) {
3224
2960
  log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3225
- // Set Redis keys to block crime/search
3226
2961
  for (const w of activeWorkers) {
3227
2962
  if (dmNoLs.includes(w.username) && redis) {
3228
2963
  try {
@@ -3234,18 +2969,15 @@ async function start(apiKey, apiUrl, opts = {}) {
3234
2969
  }
3235
2970
  if (dmUnknown.length > 0) {
3236
2971
  log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
3237
- // Crime/search on these accounts will be skipped via safety hold until the live
3238
- // DM gateway listener detects a death (→ sets count) or confirms clean.
3239
2972
  }
3240
2973
  const dmSummaryParts = [];
3241
2974
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3242
2975
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3243
2976
  if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
3244
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups'}`);
3245
- console.log('');
3246
-
3247
- console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
2977
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
3248
2978
 
2979
+ // ── Phase 3: Start grind loops ───────────────────────────────────
2980
+ console.log(` ${rgb(139, 92, 246)}>>>${c.reset} Starting grind loops...`);
3249
2981
  // Phase 3: Start all grind loops (only for valid workers)
3250
2982
  for (const w of activeWorkers) {
3251
2983
  if (!shutdownCalled) w.grindLoop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.72.0",
3
+ "version": "7.74.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"