dankgrinder 8.2.0 → 8.10.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
@@ -94,20 +94,10 @@ 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
-
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
- }
111
101
  // ── Safe options for search/crime ──────────────────────────
112
102
  // Object.freeze → V8 marks these as immutable, enabling inline caching
113
103
  // and preventing accidental mutation across 10K worker instances.
@@ -282,6 +272,17 @@ function progressBar(value, max, width, filledColor, emptyColor) {
282
272
  return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
283
273
  }
284
274
 
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
+
285
286
  // ── Box drawing helpers ──────────────────────────────────────
286
287
  const BOX = {
287
288
  tl: '╭', tr: '╮', bl: '╰', br: '╯',
@@ -1388,7 +1389,9 @@ class AccountWorker {
1388
1389
 
1389
1390
  // Update Redis with findings
1390
1391
  if (redis) {
1391
- try {
1392
+ if (currentLevel > 0) {
1393
+ await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1394
+ this._level = currentLevel;
1392
1395
  if (currentLevel > 0) {
1393
1396
  await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1394
1397
  this._level = currentLevel;
@@ -1401,19 +1404,17 @@ class AccountWorker {
1401
1404
  await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1402
1405
  }
1403
1406
  }
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]));
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
+ }
1412
1414
  }
1413
1415
  }
1416
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1414
1417
  }
1415
- return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1416
- }
1417
1418
 
1418
1419
  // ── Run Single Command ──────────────────────────────────────
1419
1420
  // Each modular command handler sends the command, waits for response,
@@ -2748,6 +2749,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2748
2749
  REDIS_URL = process.env.REDIS_URL || '';
2749
2750
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2750
2751
 
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
+
2751
2756
  // Detect zlib-sync availability
2752
2757
  let hasZlib = false;
2753
2758
  try { require('zlib-sync'); hasZlib = true; } catch {}
@@ -2760,6 +2765,8 @@ async function start(apiKey, apiUrl, opts = {}) {
2760
2765
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
2761
2766
  ` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
2762
2767
  );
2768
+ console.log(bar);
2769
+
2763
2770
  log('info', `${c.dim}Fetching accounts...${c.reset}`);
2764
2771
 
2765
2772
  const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
@@ -2824,7 +2831,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2824
2831
 
2825
2832
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
2826
2833
  if (REDIS_URL) {
2827
- rawLogger.init(redis);
2834
+ rawLogger.init(REDIS_URL).catch(() => {});
2828
2835
  // Listen for DM events across all accounts — update worker state + dashboard LIVE
2829
2836
  rawLogger.onDmEvent((event, raw) => {
2830
2837
  const channelId = raw.channel_id;
@@ -2873,8 +2880,110 @@ async function start(apiKey, apiUrl, opts = {}) {
2873
2880
  console.log(` ${checks.join(' ')}`);
2874
2881
  console.log('');
2875
2882
 
2876
- // ── Phase 1: Login accounts ─────────────────────────────────────────
2877
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
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
2987
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2879
2988
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2880
2989
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
@@ -2890,58 +2999,203 @@ async function start(apiKey, apiUrl, opts = {}) {
2890
2999
  const worker = new AccountWorker(acc, i + idx);
2891
3000
  workers.push(worker);
2892
3001
  workerMap.set(acc.id, worker);
3002
+ loginStates[i + idx].worker = worker;
2893
3003
  await worker.start();
3004
+ finalizeLoginLine(i + idx, worker);
2894
3005
  }));
2895
3006
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2896
3007
  hintGC();
2897
3008
  }
2898
3009
 
3010
+ clearInterval(loginSpinnerInterval);
2899
3011
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2900
3012
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2901
3013
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2902
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${loginDone}/${accounts.length} accounts connected`);
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('');
2903
3016
  if (invalidWorkers.length > 0) {
2904
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
2905
- for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
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
3020
  }
2907
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login`);
3021
+ if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2908
3022
 
2909
3023
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2910
3024
 
2911
- // ── Phase 2: Inventory check ─────────────────────────────────────
2912
- console.log(` ${rgb(34, 211, 238)}·${c.reset} Checking inventory (${activeWorkers.length} accounts)...`);
2913
- let invDone = 0, invFailed = 0;
2914
- await Promise.all(activeWorkers.map(async (w) => {
2915
- try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2916
- catch { invFailed++; return; }
2917
- invDone++;
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++;
2918
3095
  }));
2919
3096
 
3097
+ clearInterval(invSpinnerInterval);
3098
+ process.stdout.write(`\r\x1b[2K`);
3099
+
2920
3100
  if (invFailed > 0) {
2921
- console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed not starting grind loops`);
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}`);
2922
3103
  return;
2923
3104
  }
2924
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} Inventory: ${invDone}/${activeWorkers.length} clear`);
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('');
2925
3107
 
2926
- // ── Phase 2.5: Balance check ────────────────────────────────────
2927
- console.log(` ${rgb(251, 191, 36)}·${c.reset} Checking balance (${activeWorkers.length} accounts)...`);
2928
- await Promise.all(activeWorkers.map(async (w) => {
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) => {
2929
3163
  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++;
2930
3178
  }));
2931
3179
 
3180
+ clearInterval(balSpinnerInterval);
3181
+ process.stdout.write(`\r\x1b[2K`);
3182
+
2932
3183
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
2933
3184
  for (const w of activeWorkers) {
2934
3185
  totalWallet += w.stats?.balance || 0;
2935
3186
  totalBank += w.stats?.bankBalance || 0;
2936
3187
  if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
2937
3188
  }
2938
- 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}`);
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}`);
2939
3190
  if (noLifesaverAccounts.length > 0) {
2940
- console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
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(', ')}`);
2941
3192
  }
3193
+ console.log('');
2942
3194
 
2943
- // ── Phase 2.75: DM history check ────────────────────────────────
2944
- console.log(` ${rgb(139, 92, 246)}·${c.reset} Checking DM history...`);
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
3199
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
2946
3200
  for (const w of activeWorkers) {
2947
3201
  try {
@@ -2950,12 +3204,25 @@ async function start(apiKey, apiUrl, opts = {}) {
2950
3204
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
2951
3205
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
2952
3206
  if (dm.lifesavers === -1) dmUnknown.push(w.username);
3207
+ // Store level and lifesaver for dashboard
2953
3208
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
2954
3209
  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
+ }
2955
3221
  } catch {}
2956
3222
  }
2957
3223
  if (dmNoLs.length > 0) {
2958
3224
  log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3225
+ // Set Redis keys to block crime/search
2959
3226
  for (const w of activeWorkers) {
2960
3227
  if (dmNoLs.includes(w.username) && redis) {
2961
3228
  try {
@@ -2967,15 +3234,18 @@ async function start(apiKey, apiUrl, opts = {}) {
2967
3234
  }
2968
3235
  if (dmUnknown.length > 0) {
2969
3236
  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.
2970
3239
  }
2971
3240
  const dmSummaryParts = [];
2972
3241
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
2973
3242
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
2974
3243
  if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
2975
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
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])}`);
2976
3248
 
2977
- // ── Phase 3: Start grind loops ───────────────────────────────────
2978
- console.log(` ${rgb(139, 92, 246)}>>>${c.reset} Starting grind loops...`);
2979
3249
  // Phase 3: Start all grind loops (only for valid workers)
2980
3250
  for (const w of activeWorkers) {
2981
3251
  if (!shutdownCalled) w.grindLoop();
package/lib/rawLogger.js CHANGED
@@ -30,14 +30,7 @@ const memRing = [];
30
30
  let memIdx = 0;
31
31
 
32
32
  // ── Redis init ──
33
- async function init(redisUrlOrInstance) {
34
- // Support passing an existing Redis instance directly (preferred)
35
- if (redisUrlOrInstance && typeof redisUrlOrInstance.status !== 'undefined') {
36
- redis = redisUrlOrInstance;
37
- redisReady = redis.status === 'ready';
38
- return;
39
- }
40
- const redisUrl = redisUrlOrInstance;
33
+ async function init(redisUrl) {
41
34
  if (!redisUrl) {
42
35
  console.log('[rawLogger] No Redis URL — raw logging disabled');
43
36
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.2.0",
3
+ "version": "8.10.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"