dankgrinder 8.16.0 → 8.18.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 +59 -404
  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,37 +2646,23 @@ 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);
2716
2656
  while (!data) {
2717
- log('error', `Cannot connect to API`);
2718
- log('warn', `Will retry in 10s (check internet/API URL if this repeats).`);
2657
+ console.log('Cannot connect to API - retrying in 10s...');
2719
2658
  await new Promise((r) => setTimeout(r, 10000));
2720
2659
  data = await fetchConfig(4, 2000, fetchOpts);
2721
2660
  }
2722
2661
  if (data && data.error) {
2723
- log('error', `${data.error}`);
2662
+ console.log(`API error: ${data.error}`);
2724
2663
  return;
2725
2664
  }
2665
+ console.log(`Fetched ${data.accounts?.length || 0} accounts`);
2726
2666
 
2727
2667
  // Cloud mode: post heartbeat every 30s
2728
2668
  if (CLOUD_MODE) {
@@ -2753,24 +2693,21 @@ async function start(apiKey, apiUrl, opts = {}) {
2753
2693
 
2754
2694
  let { accounts } = data;
2755
2695
  if (!accounts || accounts.length === 0) {
2756
- log('error', 'No active accounts. Add them in the dashboard.');
2696
+ console.log('No active accounts. Add them in the dashboard.');
2757
2697
  return;
2758
2698
  }
2699
+ console.log(`Processing ${accounts.length} accounts...`);
2759
2700
 
2760
2701
  // Cluster mode: filter to only accounts this node can claim
2761
2702
  if (CLUSTER_ENABLED) {
2762
2703
  const totalBefore = accounts.length;
2763
2704
  accounts = await filterClaimableAccounts(accounts);
2764
- log('info', `${c.dim}Cluster: claimed ${accounts.length}/${totalBefore} accounts (others owned by peer nodes)${c.reset}`);
2705
+ console.log(`Cluster: claimed ${accounts.length}/${totalBefore} accounts`);
2765
2706
  if (accounts.length === 0) {
2766
- log('warn', 'All accounts claimed by other nodes. Waiting for accounts...');
2707
+ console.log('All accounts claimed by other nodes. Waiting...');
2767
2708
  }
2768
2709
  }
2769
2710
 
2770
- const checks = [];
2771
- checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2772
- if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
2773
-
2774
2711
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
2775
2712
  if (REDIS_URL) {
2776
2713
  rawLogger.init(REDIS_URL).catch(() => {});
@@ -2822,116 +2759,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2822
2759
  console.log(` ${checks.join(' ')}`);
2823
2760
  console.log('');
2824
2761
 
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
-
2762
+ // ── Phase 1: Login ─────────────────────────────────────────────
2929
2763
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2930
2764
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2931
2765
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
2932
2766
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2933
2767
  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
2768
 
2769
+ console.log(`Logging in ${accounts.length} accounts...`);
2935
2770
  const BATCH_SIZE = 10;
2936
2771
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2937
2772
  if (shutdownCalled) break;
@@ -2941,260 +2776,88 @@ async function start(apiKey, apiUrl, opts = {}) {
2941
2776
  const worker = new AccountWorker(acc, i + idx);
2942
2777
  workers.push(worker);
2943
2778
  workerMap.set(acc.id, worker);
2944
- loginStates[i + idx].worker = worker;
2945
2779
  await worker.start();
2946
- finalizeLoginLine(i + idx, worker);
2780
+ if (worker._tokenInvalid) {
2781
+ console.log(` [${i + idx + 1}] FAIL - invalid token: ${acc.label || acc.id}`);
2782
+ } else if (worker.channel) {
2783
+ console.log(` [${i + idx + 1}] OK - ${worker.username}`);
2784
+ } else {
2785
+ console.log(` [${i + idx + 1}] TIMEOUT`);
2786
+ }
2947
2787
  }));
2948
2788
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2949
2789
  hintGC();
2950
2790
  }
2951
2791
 
2952
- clearInterval(loginSpinnerInterval);
2953
2792
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2954
2793
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2955
2794
  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('');
2795
+ console.log(`Login complete: ${loginDone}/${accounts.length} connected`);
2958
2796
  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('');
2797
+ for (const w of invalidWorkers) console.log(` FAIL - invalid token: ${w.account.label || w.account.id}`);
2798
+ }
2799
+ if (timedOutWorkers.length > 0) {
2800
+ console.log(` WARN - ${timedOutWorkers.length} timed out (will retry in background)`);
2962
2801
  }
2963
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
2964
2802
 
2965
2803
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2966
2804
 
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++;
2805
+ // ── Phase 2: Inventory check ────────────────────────────────────
2806
+ console.log('Checking inventory...');
2807
+ let invFailed = 0;
2808
+ await Promise.all(activeWorkers.map(async (w) => {
2809
+ try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2810
+ catch { invFailed++; }
3037
2811
  }));
3038
-
3039
- clearInterval(invSpinnerInterval);
3040
- process.stdout.write(`\r\x1b[2K`);
3041
-
3042
2812
  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}`);
2813
+ console.log(`Inventory failed for ${invFailed} accounts. Not starting grind loops.`);
3045
2814
  return;
3046
2815
  }
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);
2816
+ console.log('Inventory check complete');
3103
2817
 
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
- }));
3121
-
3122
- clearInterval(balSpinnerInterval);
3123
- process.stdout.write(`\r\x1b[2K`);
3124
-
3125
- let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
2818
+ // ── Phase 2.5: Balance check ───────────────────────────────────
2819
+ console.log('Checking balances...');
3126
2820
  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);
2821
+ try { await w.checkBalance(true); } catch {}
3130
2822
  }
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(', ')}`);
2823
+ let totalCoins = 0;
2824
+ for (const w of activeWorkers) {
2825
+ totalCoins += w.stats?.balance || 0;
2826
+ totalCoins += w.stats?.bankBalance || 0;
3134
2827
  }
3135
- console.log('');
2828
+ console.log(`Balances: total ${totalCoins.toLocaleString()} coins across ${activeWorkers.length} accounts`);
3136
2829
 
3137
-
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 = [];
2830
+ // ── Phase 2.75: DM history check ────────────────────────────────
2831
+ console.log('Checking DM history...');
2832
+ let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
3142
2833
  for (const w of activeWorkers) {
3143
2834
  try {
3144
2835
  const dm = await w.checkDmHistory();
3145
2836
  if (dm.deaths > 0) dmDeaths += dm.deaths;
3146
2837
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3147
2838
  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
2839
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
3151
2840
  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) {
2841
+ // Block crime/search for accounts with 0 lifesavers
2842
+ if (dm.lifesavers === 0 && redis) {
3170
2843
  try {
3171
2844
  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
2845
  } catch {}
3174
2846
  }
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.
2847
+ } catch {}
3181
2848
  }
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('');
2849
+ if (dmNoLs.length > 0) console.log(` WARN - No lifesavers: ${dmNoLs.join(', ')}`);
2850
+ const parts = [];
2851
+ if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2852
+ if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
2853
+ console.log(`DM check: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
3188
2854
 
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)
2855
+ // ── Phase 3: Start grind loops ───────────────────────────────────
2856
+ console.log(`Starting ${activeWorkers.length} grind loops...`);
3192
2857
  for (const w of activeWorkers) {
3193
2858
  if (!shutdownCalled) w.grindLoop();
3194
2859
  }
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`);
2860
+ console.log(`All grind loops started. v${PKG_VERSION} | Ctrl+C to stop`);
3198
2861
 
3199
2862
  // Cluster heartbeat — lets other nodes see this node is alive
3200
2863
  if (CLUSTER_ENABLED) {
@@ -3263,31 +2926,23 @@ async function start(apiKey, apiUrl, opts = {}) {
3263
2926
  setDashboardActive(false);
3264
2927
  process.stdout.write(c.show);
3265
2928
 
3266
- const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3267
2929
  console.log('');
3268
- console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
3269
- console.log(sepBar);
2930
+ console.log('Session Summary');
3270
2931
 
3271
2932
  // Collect stats from all workers (including rotated-out ones)
3272
2933
  let finalCoins = 0;
3273
2934
  let finalCmds = 0;
3274
2935
  for (const wk of workers) {
3275
2936
  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
- );
2937
+ console.log(` ${(wk.username || '?').padEnd(18)} +${wk.stats.coins.toLocaleString().padStart(8)} coins ${wk.stats.commands} cmds ${rate}% ok`);
3282
2938
  finalCoins += wk.stats.coins || 0;
3283
2939
  finalCmds += wk.stats.commands || 0;
3284
2940
  }
3285
- console.log(sepBar);
3286
2941
 
3287
2942
  const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
3288
2943
  const avgEarn = globalEarningsEMA.get();
3289
2944
  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}`);
2945
+ console.log(`Total: +${finalCoins.toLocaleString()} coins in ${formatUptime()} | ${finalCmds} cmds | ~${cpm} cmd/m | ${memFinal}MB`);
3291
2946
  console.log('');
3292
2947
 
3293
2948
  // 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.18.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"