dankgrinder 8.16.0 → 8.17.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 +52 -394
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -94,9 +94,6 @@ const c = {
94
94
  restoreCursor: '\x1b8',
95
95
  };
96
96
 
97
- const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
98
- // Unique marker written to stdout so we can query cursor position via DSR response
99
- const MARKER = '\x1b[6n\x1b[@@MARKER@@';
100
97
  const DANK_MEMER_ID = '270904126974590976';
101
98
 
102
99
  // ── Safe options for search/crime ──────────────────────────
@@ -231,54 +228,11 @@ function gradientLine(text, from, to) {
231
228
  }
232
229
 
233
230
  function gradientText(text, from, to) {
234
- return gradientLine(text, from, to);
235
- }
236
-
237
- // ── Sparkline graph for earnings trend ───────────────────────
238
- const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
239
- function drawSparkline(data, width = 12) {
240
- if (!data || data.length === 0) return c.dim + '·····' + c.reset;
241
- const recent = data.slice(-width);
242
- const min = Math.min(...recent);
243
- const max = Math.max(...recent);
244
- const range = max - min || 1;
245
- return recent.map(v => {
246
- const idx = Math.min(7, Math.floor(((v - min) / range) * 8));
247
- const ch = SPARK_CHARS[idx] || '▁';
248
- const t = (v - min) / range;
249
- // Gradient from red->yellow->green based on relative value
250
- const r = t < 0.5 ? 239 : lerp(251, 52, (t - 0.5) * 2);
251
- const g = t < 0.5 ? lerp(68, 191, t * 2) : lerp(191, 211, (t - 0.5) * 2);
252
- const b = t < 0.5 ? lerp(68, 36, t * 2) : lerp(36, 153, (t - 0.5) * 2);
253
- return rgb(r, g, b) + ch + c.reset;
254
- }).join('');
255
- }
256
-
257
- // ── Advanced progress bar ────────────────────────────────────
258
- function progressBar(value, max, width, filledColor, emptyColor) {
259
- const pct = max > 0 ? Math.min(1, value / max) : 0;
260
- const filled = Math.round(pct * width);
261
- const empty = width - filled;
262
- const fc = filledColor || [52, 211, 153];
263
- const ec = emptyColor || [50, 50, 70];
264
- return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
265
- }
266
-
267
- // ── Animated braille spinner frames ──────────────────────────
268
- const BRAILLE_SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
269
- const BLOCK_SPIN = ['▉', '▊', '▋', '▌', '▍', '▎', '▏', '▎', '▍', '▌', '▋', '▊'];
270
- const PULSE_CHARS = ['○', '◎', '●', '◉', '●', '◎'];
271
- function getSpinner(type = 'braille') {
272
- const now = Math.floor(Date.now() / 80);
273
- if (type === 'block') return BLOCK_SPIN[now % BLOCK_SPIN.length];
274
- if (type === 'pulse') return PULSE_CHARS[now % PULSE_CHARS.length];
275
- return BRAILLE_SPIN[now % BRAILLE_SPIN.length];
231
+ return text;
276
232
  }
277
233
 
278
234
  function colorBanner() {
279
- return `
280
- DANKGRINDER v${PKG_VERSION}
281
- `;
235
+ return `DANKGRINDER v${PKG_VERSION}`;
282
236
  }
283
237
 
284
238
  // ── Simple Logging ─────────────────────────────────────────────
@@ -2692,24 +2646,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2692
2646
  REDIS_URL = process.env.REDIS_URL || '';
2693
2647
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2694
2648
 
2695
- const tw = Math.min(process.stdout.columns || 80, 78);
2696
- const bar = '─'.repeat(tw);
2649
+ const tw = 80;
2697
2650
 
2698
- // Detect zlib-sync availability
2699
- let hasZlib = false;
2700
- try { require('zlib-sync'); hasZlib = true; } catch {}
2701
-
2702
- console.log(colorBanner());
2703
- console.log(
2704
- ` ${rgb(139, 92, 246)}v${PKG_VERSION}${c.reset}` +
2705
- ` ${c.dim}·${c.reset} ${c.white}${AccountWorker.COMMAND_MAP.length} Commands${c.reset}` +
2706
- ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}${CLOUD_MODE ? 'Cloud Mode' : (CLUSTER_ENABLED ? 'Cluster Mode' : 'Standalone')}${c.reset}` +
2707
- ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
2708
- ` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
2709
- );
2710
- console.log(bar);
2711
-
2712
- log('info', `${c.dim}Fetching accounts...${c.reset}`);
2651
+ console.log(`DANKGRINDER v${PKG_VERSION} - ${AccountWorker.COMMAND_MAP.length} commands - Standalone`);
2652
+ console.log('Fetching accounts...');
2713
2653
 
2714
2654
  const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
2715
2655
  let data = await fetchConfig(4, 2000, fetchOpts);
@@ -2822,116 +2762,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2822
2762
  console.log(` ${checks.join(' ')}`);
2823
2763
  console.log('');
2824
2764
 
2825
- // ── Phase 1: Login with per-account inline rendering ─────────────────────────
2826
- const startupTw = process.stdout.columns || 90;
2827
- const colNum = 4; // " #"
2828
- const colSts = 3; // "ST"
2829
- const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
2830
- const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
2831
- const colCmds = 8;
2832
- const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
2833
-
2834
- const loginStates = accounts.map((acc, i) => ({
2835
- name: acc.label || acc.id || '?',
2836
- done: false,
2837
- failed: false,
2838
- worker: null,
2839
- }));
2840
-
2841
- let loginLines = [];
2842
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2843
- for (let i = 0; i < loginStates.length; i++) {
2844
- const s = loginStates[i];
2845
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
2846
- const name = s.name.substring(0, colName).padEnd(colName);
2847
- const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
2848
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
2849
- loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
2850
- }
2851
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2852
- for (const l of loginLines) console.log(l);
2853
-
2854
- // Dynamically capture the starting row of the login table via DSR.
2855
- // Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
2856
- // of the visible "@MARKER@@" text portion, which was causing the DSR
2857
- // response to be swallowed or delayed.
2858
- let loginBaseRow = 1;
2859
- const captureLoginRow = () => new Promise(resolve => {
2860
- const chunks = [];
2861
- const handler = (chunk) => {
2862
- chunks.push(chunk);
2863
- const raw = chunks.join('');
2864
- const m = raw.match(/\x1b\[(\d+);\d+R/);
2865
- if (m) {
2866
- process.stdin.removeListener('data', handler);
2867
- loginBaseRow = parseInt(m[1], 10) + 1;
2868
- resolve();
2869
- }
2870
- };
2871
- process.stdin.on('data', handler);
2872
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
2873
- process.stderr.write(MARKER);
2874
- setTimeout(resolve, 50);
2875
- });
2876
- await captureLoginRow();
2877
-
2878
- let loginPending = new Array(accounts.length).fill(true);
2879
- const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
2880
-
2881
- const drawLoginSpinners = () => {
2882
- for (let i = 0; i < loginPending.length; i++) {
2883
- if (!loginPending[i]) continue;
2884
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
2885
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
2886
- const name = loginStates[i].name.substring(0, colName).padEnd(colName);
2887
- const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
2888
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
2889
- const row = loginBaseRow + 1 + i; // +1 skips the top border line
2890
- moveToRow(row);
2891
- process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
2892
- }
2893
- // Move cursor back to bottom to avoid overwriting the bottom border
2894
- const lastRow = loginBaseRow + 1 + accounts.length + 1;
2895
- moveToRow(lastRow);
2896
- };
2897
- const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
2898
-
2899
- const finalizeLoginLine = (idx, worker) => {
2900
- if (!loginPending[idx]) return;
2901
- loginPending[idx] = false;
2902
- const s = loginStates[idx];
2903
- s.done = true;
2904
- s.worker = worker;
2905
-
2906
- const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
2907
- const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
2908
- let sts, guild, cmds;
2909
- if (worker._tokenInvalid) {
2910
- sts = `${rgb(239, 68, 68)}✗${c.reset}`;
2911
- guild = 'INVALID'.padEnd(colGuild);
2912
- cmds = '···'.padEnd(colCmds);
2913
- s.failed = true;
2914
- } else if (worker.channel) {
2915
- sts = `${rgb(52, 211, 153)}✓${c.reset}`;
2916
- const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
2917
- guild = gn.padEnd(colGuild);
2918
- cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
2919
- } else {
2920
- sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
2921
- guild = 'timeout'.padEnd(colGuild);
2922
- cmds = '···'.padEnd(colCmds);
2923
- }
2924
- const row = loginBaseRow + 1 + idx; // +1 skips the top border line
2925
- moveToRow(row);
2926
- process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
2927
- };
2928
-
2765
+ // ── Phase 1: Login ─────────────────────────────────────────────
2929
2766
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2930
2767
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2931
2768
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
2932
2769
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2933
2770
  const randomLoginGap = () => LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS ? LOGIN_GAP_MIN_MS : LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2934
2771
 
2772
+ console.log(`Logging in ${accounts.length} accounts...`);
2935
2773
  const BATCH_SIZE = 10;
2936
2774
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2937
2775
  if (shutdownCalled) break;
@@ -2941,260 +2779,88 @@ async function start(apiKey, apiUrl, opts = {}) {
2941
2779
  const worker = new AccountWorker(acc, i + idx);
2942
2780
  workers.push(worker);
2943
2781
  workerMap.set(acc.id, worker);
2944
- loginStates[i + idx].worker = worker;
2945
2782
  await worker.start();
2946
- finalizeLoginLine(i + idx, worker);
2783
+ if (worker._tokenInvalid) {
2784
+ console.log(` [${i + idx + 1}] FAIL - invalid token: ${acc.label || acc.id}`);
2785
+ } else if (worker.channel) {
2786
+ console.log(` [${i + idx + 1}] OK - ${worker.username}`);
2787
+ } else {
2788
+ console.log(` [${i + idx + 1}] TIMEOUT`);
2789
+ }
2947
2790
  }));
2948
2791
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2949
2792
  hintGC();
2950
2793
  }
2951
2794
 
2952
- clearInterval(loginSpinnerInterval);
2953
2795
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2954
2796
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2955
2797
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2956
- 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}`);
2957
- console.log('');
2798
+ console.log(`Login complete: ${loginDone}/${accounts.length} connected`);
2958
2799
  if (invalidWorkers.length > 0) {
2959
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
2960
- for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
2961
- console.log('');
2800
+ for (const w of invalidWorkers) console.log(` FAIL - invalid token: ${w.account.label || w.account.id}`);
2801
+ }
2802
+ if (timedOutWorkers.length > 0) {
2803
+ console.log(` WARN - ${timedOutWorkers.length} timed out (will retry in background)`);
2962
2804
  }
2963
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2964
2805
 
2965
2806
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2966
2807
 
2967
- // ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
2968
- const iColNum = 4;
2969
- const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
2970
- const iColItems = 8;
2971
- const iColVal = 16;
2972
- const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
2973
-
2974
- // Print a unique marker, query its position, then overwrite it with the table
2975
- // Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
2976
- let invBaseRow = 1;
2977
- const captureRow = () => new Promise(resolve => {
2978
- const chunks = [];
2979
- const handler = (chunk) => {
2980
- chunks.push(chunk);
2981
- const raw = chunks.join('');
2982
- const m = raw.match(/\x1b\[(\d+);\d+R/);
2983
- if (m) {
2984
- process.stdin.removeListener('data', handler);
2985
- invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
2986
- resolve();
2987
- }
2988
- };
2989
- process.stdin.on('data', handler);
2990
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
2991
- process.stderr.write(MARKER);
2992
- setTimeout(resolve, 50);
2993
- });
2994
- await captureRow();
2995
-
2996
- // Now print the inventory table starting at invBaseRow
2997
- const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
2998
- console.log(` ${'─'.repeat(invVis)}`);
2999
- for (let i = 0; i < activeWorkers.length; i++) {
3000
- const w = activeWorkers[i];
3001
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3002
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3003
- console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'···'.padEnd(iColVal)}${c.reset}`);
3004
- }
3005
- console.log(` ${'─'.repeat(invVis)}`);
3006
-
3007
- let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3008
- const drawInvProgress = () => {
3009
- if (invPending === 0) return;
3010
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3011
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3012
- const barW = Math.min(20, startupTw - 40);
3013
- const filled = Math.round(pct * barW);
3014
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3015
- const pctStr = `${Math.round(pct * 100)}%`;
3016
- invMoveToRow(invBaseRow);
3017
- 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`);
3018
- };
3019
- const invSpinnerInterval = setInterval(drawInvProgress, 80);
3020
-
3021
- await Promise.all(activeWorkers.map(async (w, i) => {
3022
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3023
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3024
- let invRes;
3025
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3026
- catch { invRes = { ok: false }; }
3027
- invPending--;
3028
- const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3029
- const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3030
- const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3031
- const itemStr = `${items}`.padEnd(iColItems);
3032
- const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3033
- const row = invBaseRow + 1 + i;
3034
- invMoveToRow(row);
3035
- process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3036
- if (invRes?.ok) invDone++; else invFailed++;
2808
+ // ── Phase 2: Inventory check ────────────────────────────────────
2809
+ console.log('Checking inventory...');
2810
+ let invFailed = 0;
2811
+ await Promise.all(activeWorkers.map(async (w) => {
2812
+ try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2813
+ catch { invFailed++; }
3037
2814
  }));
3038
-
3039
- clearInterval(invSpinnerInterval);
3040
- process.stdout.write(`\r\x1b[2K`);
3041
-
3042
2815
  if (invFailed > 0) {
3043
- 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}`);
3044
- log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
2816
+ console.log(`Inventory failed for ${invFailed} accounts. Not starting grind loops.`);
3045
2817
  return;
3046
2818
  }
3047
- 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}`);
3048
- console.log('');
3049
-
3050
- // ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
3051
- const bColNum = 4;
3052
- const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3053
- const bColWallet = 12;
3054
- const bColBank = 12;
3055
- const bColTotal = 14;
3056
- const bColLs = 4;
3057
- const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3058
-
3059
- // Capture starting row for balance phase
3060
- // Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
3061
- let balBaseRow = 1;
3062
- const balCaptureRow = () => new Promise(resolve => {
3063
- const chunks = [];
3064
- const handler = (chunk) => {
3065
- chunks.push(chunk);
3066
- const raw = chunks.join('');
3067
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3068
- if (m) {
3069
- process.stdin.removeListener('data', handler);
3070
- balBaseRow = parseInt(m[1], 10) + 1;
3071
- resolve();
3072
- }
3073
- };
3074
- process.stdin.on('data', handler);
3075
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3076
- process.stderr.write(MARKER);
3077
- setTimeout(resolve, 50);
3078
- });
3079
- await balCaptureRow();
3080
-
3081
- const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3082
- console.log(` ${'─'.repeat(balVis)}`);
3083
- for (let i = 0; i < activeWorkers.length; i++) {
3084
- const w = activeWorkers[i];
3085
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3086
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3087
- 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}`);
3088
- }
3089
- console.log(` ${'─'.repeat(balVis)}`);
3090
-
3091
- let balDone = 0, balPending = activeWorkers.length;
3092
- const drawBalProgress = () => {
3093
- if (balPending === 0) return;
3094
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3095
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3096
- const barW = Math.min(20, startupTw - 40);
3097
- const filled = Math.round(pct * barW);
3098
- const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3099
- balMoveToRow(balBaseRow);
3100
- 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`);
3101
- };
3102
- const balSpinnerInterval = setInterval(drawBalProgress, 80);
3103
-
3104
- await Promise.all(activeWorkers.map(async (w, i) => {
3105
- try { await w.checkBalance(true); } catch {}
3106
- balPending--;
3107
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3108
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3109
- const wallet = w.stats?.balance || 0;
3110
- const bank = w.stats?.bankBalance || 0;
3111
- const ls = w._lifesavers ?? '?';
3112
- const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3113
- const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3114
- const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3115
- const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3116
- const row = balBaseRow + 1 + i;
3117
- balMoveToRow(row);
3118
- 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`);
3119
- balDone++;
3120
- }));
2819
+ console.log('Inventory check complete');
3121
2820
 
3122
- clearInterval(balSpinnerInterval);
3123
- process.stdout.write(`\r\x1b[2K`);
3124
-
3125
- let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
2821
+ // ── Phase 2.5: Balance check ───────────────────────────────────
2822
+ console.log('Checking balances...');
3126
2823
  for (const w of activeWorkers) {
3127
- totalWallet += w.stats?.balance || 0;
3128
- totalBank += w.stats?.bankBalance || 0;
3129
- if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
2824
+ try { await w.checkBalance(true); } catch {}
3130
2825
  }
3131
- 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}`);
3132
- if (noLifesaverAccounts.length > 0) {
3133
- 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(', ')}`);
2826
+ let totalCoins = 0;
2827
+ for (const w of activeWorkers) {
2828
+ totalCoins += w.stats?.balance || 0;
2829
+ totalCoins += w.stats?.bankBalance || 0;
3134
2830
  }
3135
- console.log('');
3136
-
2831
+ console.log(`Balances: total ${totalCoins.toLocaleString()} coins across ${activeWorkers.length} accounts`);
3137
2832
 
3138
- // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3139
- const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3140
- console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3141
- let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
2833
+ // ── Phase 2.75: DM history check ────────────────────────────────
2834
+ console.log('Checking DM history...');
2835
+ let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
3142
2836
  for (const w of activeWorkers) {
3143
2837
  try {
3144
2838
  const dm = await w.checkDmHistory();
3145
2839
  if (dm.deaths > 0) dmDeaths += dm.deaths;
3146
2840
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3147
2841
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
3148
- if (dm.lifesavers === -1) dmUnknown.push(w.username);
3149
- // Store level and lifesaver for dashboard
3150
2842
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
3151
2843
  if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
3152
- const parts = [];
3153
- if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
3154
- if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
3155
- if (dm.lifesavers >= 0) {
3156
- const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3157
- parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
3158
- } else {
3159
- // Unknown lifesavers — pulse to show pending
3160
- const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3161
- parts.push(`${D}${pulse}♥?${c.reset}`);
3162
- }
3163
- } catch {}
3164
- }
3165
- if (dmNoLs.length > 0) {
3166
- log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3167
- // Set Redis keys to block crime/search
3168
- for (const w of activeWorkers) {
3169
- if (dmNoLs.includes(w.username) && redis) {
2844
+ // Block crime/search for accounts with 0 lifesavers
2845
+ if (dm.lifesavers === 0 && redis) {
3170
2846
  try {
3171
2847
  await redis.set(`dkg:lifesavers:${w.account.id}`, '0', 'EX', 86400);
3172
- await redis.set(`raw:alert:no-lifesaver:${w.channel?.id}`, '1', 'EX', 86400);
3173
2848
  } catch {}
3174
2849
  }
3175
- }
3176
- }
3177
- if (dmUnknown.length > 0) {
3178
- log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
3179
- // Crime/search on these accounts will be skipped via safety hold until the live
3180
- // DM gateway listener detects a death (→ sets count) or confirms clean.
2850
+ } catch {}
3181
2851
  }
3182
- const dmSummaryParts = [];
3183
- if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3184
- if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3185
- if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
3186
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups'}`);
3187
- console.log('');
2852
+ if (dmNoLs.length > 0) console.log(` WARN - No lifesavers: ${dmNoLs.join(', ')}`);
2853
+ const parts = [];
2854
+ if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2855
+ if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
2856
+ console.log(`DM check: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
3188
2857
 
3189
- console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
3190
-
3191
- // Phase 3: Start all grind loops (only for valid workers)
2858
+ // ── Phase 3: Start grind loops ───────────────────────────────────
2859
+ console.log(`Starting ${activeWorkers.length} grind loops...`);
3192
2860
  for (const w of activeWorkers) {
3193
2861
  if (!shutdownCalled) w.grindLoop();
3194
2862
  }
3195
-
3196
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
3197
- console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
2863
+ console.log(`All grind loops started. v${PKG_VERSION} | Ctrl+C to stop`);
3198
2864
 
3199
2865
  // Cluster heartbeat — lets other nodes see this node is alive
3200
2866
  if (CLUSTER_ENABLED) {
@@ -3263,31 +2929,23 @@ async function start(apiKey, apiUrl, opts = {}) {
3263
2929
  setDashboardActive(false);
3264
2930
  process.stdout.write(c.show);
3265
2931
 
3266
- const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3267
2932
  console.log('');
3268
- console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
3269
- console.log(sepBar);
2933
+ console.log('Session Summary');
3270
2934
 
3271
2935
  // Collect stats from all workers (including rotated-out ones)
3272
2936
  let finalCoins = 0;
3273
2937
  let finalCmds = 0;
3274
2938
  for (const wk of workers) {
3275
2939
  const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
3276
- console.log(
3277
- ` ${wk.color}${c.bold}${(wk.username || '?').padEnd(18)}${c.reset}` +
3278
- ` ${rgb(52, 211, 153)}+⏣ ${wk.stats.coins.toLocaleString().padStart(8)}${c.reset}` +
3279
- ` ${c.dim}${wk.stats.commands.toString().padStart(4)} cmds${c.reset}` +
3280
- ` ${c.dim}${rate}% success${c.reset}`
3281
- );
2940
+ console.log(` ${(wk.username || '?').padEnd(18)} +${wk.stats.coins.toLocaleString().padStart(8)} coins ${wk.stats.commands} cmds ${rate}% ok`);
3282
2941
  finalCoins += wk.stats.coins || 0;
3283
2942
  finalCmds += wk.stats.commands || 0;
3284
2943
  }
3285
- console.log(sepBar);
3286
2944
 
3287
2945
  const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
3288
2946
  const avgEarn = globalEarningsEMA.get();
3289
2947
  const cpm = globalCmdRate.getRate().toFixed(1);
3290
- console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${finalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()} | ${finalCmds} cmds | ~${cpm} cmd/m | ${memFinal}MB | avg earn ⏣ ${Math.round(avgEarn)}${c.reset}`);
2948
+ console.log(`Total: +${finalCoins.toLocaleString()} coins in ${formatUptime()} | ${finalCmds} cmds | ~${cpm} cmd/m | ${memFinal}MB`);
3291
2949
  console.log('');
3292
2950
 
3293
2951
  // Release all cluster claims before stopping workers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.16.0",
3
+ "version": "8.17.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"