dankgrinder 7.73.0 → 7.75.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,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: '╯',
@@ -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 {}
@@ -2765,8 +2760,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2765
2760
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
2766
2761
  ` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
2767
2762
  );
2768
- console.log(bar);
2769
-
2770
2763
  log('info', `${c.dim}Fetching accounts...${c.reset}`);
2771
2764
 
2772
2765
  const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
@@ -2831,7 +2824,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2831
2824
 
2832
2825
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
2833
2826
  if (REDIS_URL) {
2834
- rawLogger.init(REDIS_URL).catch(() => {});
2827
+ rawLogger.init(redis);
2835
2828
  // Listen for DM events across all accounts — update worker state + dashboard LIVE
2836
2829
  rawLogger.onDmEvent((event, raw) => {
2837
2830
  const channelId = raw.channel_id;
@@ -2880,110 +2873,8 @@ async function start(apiKey, apiUrl, opts = {}) {
2880
2873
  console.log(` ${checks.join(' ')}`);
2881
2874
  console.log('');
2882
2875
 
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
-
2876
+ // ── Phase 1: Login accounts ─────────────────────────────────────────
2877
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
2987
2878
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2988
2879
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2989
2880
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
@@ -2999,203 +2890,58 @@ async function start(apiKey, apiUrl, opts = {}) {
2999
2890
  const worker = new AccountWorker(acc, i + idx);
3000
2891
  workers.push(worker);
3001
2892
  workerMap.set(acc.id, worker);
3002
- loginStates[i + idx].worker = worker;
3003
2893
  await worker.start();
3004
- finalizeLoginLine(i + idx, worker);
3005
2894
  }));
3006
2895
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3007
2896
  hintGC();
3008
2897
  }
3009
2898
 
3010
- clearInterval(loginSpinnerInterval);
3011
2899
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3012
2900
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3013
2901
  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('');
2902
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${loginDone}/${accounts.length} accounts connected`);
3016
2903
  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('');
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`);
3020
2906
  }
3021
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2907
+ if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login`);
3022
2908
 
3023
2909
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3024
2910
 
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++;
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++;
3095
2918
  }));
3096
2919
 
3097
- clearInterval(invSpinnerInterval);
3098
- process.stdout.write(`\r\x1b[2K`);
3099
-
3100
2920
  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}`);
2921
+ console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed not starting grind loops`);
3103
2922
  return;
3104
2923
  }
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('');
3107
-
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);
2924
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} Inventory: ${invDone}/${activeWorkers.length} clear`);
3161
2925
 
3162
- await Promise.all(activeWorkers.map(async (w, i) => {
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) => {
3163
2929
  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
2930
  }));
3179
2931
 
3180
- clearInterval(balSpinnerInterval);
3181
- process.stdout.write(`\r\x1b[2K`);
3182
-
3183
2932
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3184
2933
  for (const w of activeWorkers) {
3185
2934
  totalWallet += w.stats?.balance || 0;
3186
2935
  totalBank += w.stats?.bankBalance || 0;
3187
2936
  if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
3188
2937
  }
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}`);
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}`);
3190
2939
  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(', ')}`);
2940
+ console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
3192
2941
  }
3193
- console.log('');
3194
-
3195
2942
 
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}`);
2943
+ // ── Phase 2.75: DM history check ────────────────────────────────
2944
+ console.log(` ${rgb(139, 92, 246)}·${c.reset} Checking DM history...`);
3199
2945
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
3200
2946
  for (const w of activeWorkers) {
3201
2947
  try {
@@ -3204,25 +2950,12 @@ async function start(apiKey, apiUrl, opts = {}) {
3204
2950
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3205
2951
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
3206
2952
  if (dm.lifesavers === -1) dmUnknown.push(w.username);
3207
- // Store level and lifesaver for dashboard
3208
2953
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
3209
2954
  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
2955
  } catch {}
3222
2956
  }
3223
2957
  if (dmNoLs.length > 0) {
3224
2958
  log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3225
- // Set Redis keys to block crime/search
3226
2959
  for (const w of activeWorkers) {
3227
2960
  if (dmNoLs.includes(w.username) && redis) {
3228
2961
  try {
@@ -3234,18 +2967,15 @@ async function start(apiKey, apiUrl, opts = {}) {
3234
2967
  }
3235
2968
  if (dmUnknown.length > 0) {
3236
2969
  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
2970
  }
3240
2971
  const dmSummaryParts = [];
3241
2972
  if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3242
2973
  if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3243
2974
  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])}`);
2975
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
3248
2976
 
2977
+ // ── Phase 3: Start grind loops ───────────────────────────────────
2978
+ console.log(` ${rgb(139, 92, 246)}>>>${c.reset} Starting grind loops...`);
3249
2979
  // Phase 3: Start all grind loops (only for valid workers)
3250
2980
  for (const w of activeWorkers) {
3251
2981
  if (!shutdownCalled) w.grindLoop();
package/lib/rawLogger.js CHANGED
@@ -30,7 +30,14 @@ const memRing = [];
30
30
  let memIdx = 0;
31
31
 
32
32
  // ── Redis init ──
33
- async function init(redisUrl) {
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;
34
41
  if (!redisUrl) {
35
42
  console.log('[rawLogger] No Redis URL — raw logging disabled');
36
43
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.73.0",
3
+ "version": "7.75.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"