dankgrinder 8.15.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 +69 -470
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -77,13 +77,14 @@ async function sendWebhook(title, description, color = 0x5865f2) {
77
77
  }
78
78
 
79
79
  // ── Terminal Colors & ANSI ───────────────────────────────────
80
+ // All colors stripped — plain text output only
80
81
  const c = {
81
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
82
- green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m',
83
- magenta: '\x1b[35m', white: '\x1b[37m', blue: '\x1b[34m',
84
- bgGreen: '\x1b[42m', bgRed: '\x1b[41m', bgYellow: '\x1b[43m', bgCyan: '\x1b[46m',
85
- bgMagenta: '\x1b[45m', bgBlue: '\x1b[44m', bgWhite: '\x1b[47m',
86
- // Cursor control
82
+ reset: '', bold: '', dim: '', italic: '',
83
+ green: '', red: '', yellow: '', cyan: '',
84
+ magenta: '', white: '', blue: '',
85
+ bgGreen: '', bgRed: '', bgYellow: '', bgCyan: '',
86
+ bgMagenta: '', bgBlue: '', bgWhite: '',
87
+ // Cursor control (kept for functional use)
87
88
  clearLine: '\x1b[2K',
88
89
  cursorUp: (n) => `\x1b[${n}A`,
89
90
  cursorTo: (col) => `\x1b[${col}G`,
@@ -93,9 +94,6 @@ const c = {
93
94
  restoreCursor: '\x1b8',
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
 
101
99
  // ── Safe options for search/crime ──────────────────────────
@@ -220,132 +218,31 @@ async function filterClaimableAccounts(accounts) {
220
218
  return claimable;
221
219
  }
222
220
 
223
- // ── Truecolor gradient helpers ───────────────────────────────
224
- function rgb(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
225
- function bgRgb(r, g, b) { return `\x1b[48;2;${r};${g};${b}m`; }
221
+ // ── Truecolor gradient helpers (disabled — plain text only) ──
222
+ function rgb(r, g, b) { return ''; }
223
+ function bgRgb(r, g, b) { return ''; }
226
224
  function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
227
225
 
228
226
  function gradientLine(text, from, to) {
229
- const chars = [...text];
230
- const visible = chars.filter(ch => ch !== ' ').length;
231
- let ci = 0, out = '';
232
- for (const ch of chars) {
233
- if (ch === ' ') { out += ' '; continue; }
234
- const t = visible > 1 ? ci / (visible - 1) : 0;
235
- out += rgb(lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)) + ch;
236
- ci++;
237
- }
238
- return out + c.reset;
227
+ return text;
239
228
  }
240
229
 
241
230
  function gradientText(text, from, to) {
242
- return gradientLine(text, from, to);
243
- }
244
-
245
- // ── Sparkline graph for earnings trend ───────────────────────
246
- const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
247
- function drawSparkline(data, width = 12) {
248
- if (!data || data.length === 0) return c.dim + '·····' + c.reset;
249
- const recent = data.slice(-width);
250
- const min = Math.min(...recent);
251
- const max = Math.max(...recent);
252
- const range = max - min || 1;
253
- return recent.map(v => {
254
- const idx = Math.min(7, Math.floor(((v - min) / range) * 8));
255
- const ch = SPARK_CHARS[idx] || '▁';
256
- const t = (v - min) / range;
257
- // Gradient from red->yellow->green based on relative value
258
- const r = t < 0.5 ? 239 : lerp(251, 52, (t - 0.5) * 2);
259
- const g = t < 0.5 ? lerp(68, 191, t * 2) : lerp(191, 211, (t - 0.5) * 2);
260
- const b = t < 0.5 ? lerp(68, 36, t * 2) : lerp(36, 153, (t - 0.5) * 2);
261
- return rgb(r, g, b) + ch + c.reset;
262
- }).join('');
263
- }
264
-
265
- // ── Advanced progress bar ────────────────────────────────────
266
- function progressBar(value, max, width, filledColor, emptyColor) {
267
- const pct = max > 0 ? Math.min(1, value / max) : 0;
268
- const filled = Math.round(pct * width);
269
- const empty = width - filled;
270
- const fc = filledColor || [52, 211, 153];
271
- const ec = emptyColor || [50, 50, 70];
272
- return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
273
- }
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
-
286
- // ── Box drawing helpers ──────────────────────────────────────
287
- const BOX = {
288
- tl: '╭', tr: '╮', bl: '╰', br: '╯',
289
- h: '─', v: '│', hBold: '━', vBold: '┃',
290
- dtl: '╔', dtr: '╗', dbl: '╚', dbr: '╝', dh: '═', dv: '║',
291
- cross: '┼', tee: '├', teeR: '┤', teeD: '┬', teeU: '┴',
292
- };
293
-
294
- function boxTop(w, color) { return color + BOX.dtl + BOX.dh.repeat(w - 2) + BOX.dtr + c.reset; }
295
- function boxMid(w, color) { return color + BOX.tee + BOX.h.repeat(w - 2) + BOX.teeR + c.reset; }
296
- function boxBot(w, color) { return color + BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr + c.reset; }
297
- function boxLine(content, w, color) {
298
- const stripped = content.replace(/\x1b\[[0-9;]*m/g, '');
299
- const pad = Math.max(0, w - 4 - stripped.length);
300
- return color + BOX.dv + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
231
+ return text;
301
232
  }
302
- function thinLine(w) { return ' ' + c.dim + BOX.h.repeat(w - 4) + c.reset; }
303
-
304
- const BANNER_RAW = [
305
- ' ██████╗ █████╗ ███╗ ██╗██╗ ██╗',
306
- ' ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝',
307
- ' ██║ ██║███████║██╔██╗ ██║█████╔╝ ',
308
- ' ██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ',
309
- ' ██████╔╝██║ ██║██║ ╚████║██║ ██╗',
310
- ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝',
311
- ' ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗ ',
312
- ' ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗',
313
- ' ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝',
314
- ' ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗',
315
- ' ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║',
316
- ' ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝',
317
- ];
318
233
 
319
234
  function colorBanner() {
320
- const topColor = [192, 132, 252];
321
- const midColor = [139, 92, 246];
322
- const botColor = [34, 211, 238];
323
- const n = BANNER_RAW.length;
324
- let out = '\n';
325
- for (let i = 0; i < n; i++) {
326
- const t = i / (n - 1);
327
- const from = t < 0.5
328
- ? [lerp(topColor[0], midColor[0], t * 2), lerp(topColor[1], midColor[1], t * 2), lerp(topColor[2], midColor[2], t * 2)]
329
- : [lerp(midColor[0], botColor[0], (t - 0.5) * 2), lerp(midColor[1], botColor[1], (t - 0.5) * 2), lerp(midColor[2], botColor[2], (t - 0.5) * 2)];
330
- const to = t < 0.5
331
- ? [lerp(236, 168, t * 2), lerp(72, 85, t * 2), lerp(153, 247, t * 2)]
332
- : [lerp(168, 6, (t - 0.5) * 2), lerp(85, 182, (t - 0.5) * 2), lerp(247, 212, (t - 0.5) * 2)];
333
- out += c.bold + gradientLine(BANNER_RAW[i], from, to) + '\n';
334
- }
335
- return out;
235
+ return `DANKGRINDER v${PKG_VERSION}`;
336
236
  }
337
237
 
338
238
  // ── Simple Logging ─────────────────────────────────────────────
339
239
  function log(type, msg, label) {
340
- const colorIcons = {
341
- info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
342
- error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
343
- cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
344
- buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
345
- debug: `${c.dim}·${c.reset}`,
240
+ const icons = {
241
+ info: '.', success: '[OK]', error: '[X]', warn: '[!]',
242
+ cmd: '>', coin: '$', buy: '#', bal: '*', debug: '.',
346
243
  };
347
244
  const tagCol = label ? `${label} ` : '';
348
- console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
245
+ console.log(` ${icons[type] || icons.info} ${tagCol}${msg}`);
349
246
  }
350
247
 
351
248
  async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
@@ -2737,38 +2634,22 @@ captchaDetector.build();
2737
2634
  // ══════════════════════════════════════════════════════════════
2738
2635
 
2739
2636
  async function start(apiKey, apiUrl, opts = {}) {
2637
+ console.log('DANKGRINDER starting...');
2740
2638
  CLOUD_ADMIN_KEY = process.env.CLOUD_ADMIN_KEY || '';
2741
2639
  API_KEY = apiKey;
2742
2640
  API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
2743
2641
  const CLOUD_MODE = opts.cloud === true;
2744
2642
 
2745
2643
  if (CLOUD_MODE) {
2746
- // In cloud mode, API_KEY is the CLOUD_ADMIN_KEY — not used for user auth.
2747
- // Per-account keys are fetched per-account from /api/cloud/grinders.
2748
- console.log('🌥️ Starting in CLOUD MODE — grinding all cloud-enabled accounts');
2644
+ console.log('Starting in CLOUD MODE');
2749
2645
  }
2750
2646
  REDIS_URL = process.env.REDIS_URL || '';
2751
2647
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2752
2648
 
2753
- process.stdout.write('\x1b[2J\x1b[H');
2754
- const tw = Math.min(process.stdout.columns || 80, 78);
2755
- const bar = c.dim + '─'.repeat(tw) + c.reset;
2649
+ const tw = 80;
2756
2650
 
2757
- // Detect zlib-sync availability
2758
- let hasZlib = false;
2759
- try { require('zlib-sync'); hasZlib = true; } catch {}
2760
-
2761
- console.log(colorBanner());
2762
- console.log(
2763
- ` ${rgb(139, 92, 246)}v${PKG_VERSION}${c.reset}` +
2764
- ` ${c.dim}·${c.reset} ${c.white}${AccountWorker.COMMAND_MAP.length} Commands${c.reset}` +
2765
- ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}${CLOUD_MODE ? 'Cloud Mode' : (CLUSTER_ENABLED ? 'Cluster Mode' : 'Standalone')}${c.reset}` +
2766
- ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
2767
- ` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
2768
- );
2769
- console.log(bar);
2770
-
2771
- 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...');
2772
2653
 
2773
2654
  const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
2774
2655
  let data = await fetchConfig(4, 2000, fetchOpts);
@@ -2881,116 +2762,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2881
2762
  console.log(` ${checks.join(' ')}`);
2882
2763
  console.log('');
2883
2764
 
2884
- // ── Phase 1: Login with per-account inline rendering ─────────────────────────
2885
- const startupTw = process.stdout.columns || 90;
2886
- const colNum = 4; // " #"
2887
- const colSts = 3; // "ST"
2888
- const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
2889
- const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
2890
- const colCmds = 8;
2891
- const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
2892
-
2893
- const loginStates = accounts.map((acc, i) => ({
2894
- name: acc.label || acc.id || '?',
2895
- done: false,
2896
- failed: false,
2897
- worker: null,
2898
- }));
2899
-
2900
- let loginLines = [];
2901
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2902
- for (let i = 0; i < loginStates.length; i++) {
2903
- const s = loginStates[i];
2904
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
2905
- const name = s.name.substring(0, colName).padEnd(colName);
2906
- const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
2907
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
2908
- loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
2909
- }
2910
- loginLines.push(` ${'─'.repeat(loginVis)}`);
2911
- for (const l of loginLines) console.log(l);
2912
-
2913
- // Dynamically capture the starting row of the login table via DSR.
2914
- // Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
2915
- // of the visible "@MARKER@@" text portion, which was causing the DSR
2916
- // response to be swallowed or delayed.
2917
- let loginBaseRow = 1;
2918
- const captureLoginRow = () => new Promise(resolve => {
2919
- const chunks = [];
2920
- const handler = (chunk) => {
2921
- chunks.push(chunk);
2922
- const raw = chunks.join('');
2923
- const m = raw.match(/\x1b\[(\d+);\d+R/);
2924
- if (m) {
2925
- process.stdin.removeListener('data', handler);
2926
- loginBaseRow = parseInt(m[1], 10) + 1;
2927
- resolve();
2928
- }
2929
- };
2930
- process.stdin.on('data', handler);
2931
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
2932
- process.stderr.write(MARKER);
2933
- setTimeout(resolve, 50);
2934
- });
2935
- await captureLoginRow();
2936
-
2937
- let loginPending = new Array(accounts.length).fill(true);
2938
- const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
2939
-
2940
- const drawLoginSpinners = () => {
2941
- for (let i = 0; i < loginPending.length; i++) {
2942
- if (!loginPending[i]) continue;
2943
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
2944
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
2945
- const name = loginStates[i].name.substring(0, colName).padEnd(colName);
2946
- const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
2947
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
2948
- const row = loginBaseRow + 1 + i; // +1 skips the top border line
2949
- moveToRow(row);
2950
- process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
2951
- }
2952
- // Move cursor back to bottom to avoid overwriting the bottom border
2953
- const lastRow = loginBaseRow + 1 + accounts.length + 1;
2954
- moveToRow(lastRow);
2955
- };
2956
- const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
2957
-
2958
- const finalizeLoginLine = (idx, worker) => {
2959
- if (!loginPending[idx]) return;
2960
- loginPending[idx] = false;
2961
- const s = loginStates[idx];
2962
- s.done = true;
2963
- s.worker = worker;
2964
-
2965
- const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
2966
- const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
2967
- let sts, guild, cmds;
2968
- if (worker._tokenInvalid) {
2969
- sts = `${rgb(239, 68, 68)}✗${c.reset}`;
2970
- guild = 'INVALID'.padEnd(colGuild);
2971
- cmds = '···'.padEnd(colCmds);
2972
- s.failed = true;
2973
- } else if (worker.channel) {
2974
- sts = `${rgb(52, 211, 153)}✓${c.reset}`;
2975
- const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
2976
- guild = gn.padEnd(colGuild);
2977
- cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
2978
- } else {
2979
- sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
2980
- guild = 'timeout'.padEnd(colGuild);
2981
- cmds = '···'.padEnd(colCmds);
2982
- }
2983
- const row = loginBaseRow + 1 + idx; // +1 skips the top border line
2984
- moveToRow(row);
2985
- process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
2986
- };
2987
-
2765
+ // ── Phase 1: Login ─────────────────────────────────────────────
2988
2766
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
2989
2767
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
2990
2768
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
2991
2769
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2992
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));
2993
2771
 
2772
+ console.log(`Logging in ${accounts.length} accounts...`);
2994
2773
  const BATCH_SIZE = 10;
2995
2774
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2996
2775
  if (shutdownCalled) break;
@@ -3000,260 +2779,88 @@ async function start(apiKey, apiUrl, opts = {}) {
3000
2779
  const worker = new AccountWorker(acc, i + idx);
3001
2780
  workers.push(worker);
3002
2781
  workerMap.set(acc.id, worker);
3003
- loginStates[i + idx].worker = worker;
3004
2782
  await worker.start();
3005
- 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
+ }
3006
2790
  }));
3007
2791
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3008
2792
  hintGC();
3009
2793
  }
3010
2794
 
3011
- clearInterval(loginSpinnerInterval);
3012
2795
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3013
2796
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3014
2797
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3015
- 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}`);
3016
- console.log('');
2798
+ console.log(`Login complete: ${loginDone}/${accounts.length} connected`);
3017
2799
  if (invalidWorkers.length > 0) {
3018
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3019
- for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3020
- 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)`);
3021
2804
  }
3022
- if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3023
2805
 
3024
2806
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3025
2807
 
3026
- // ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
3027
- const iColNum = 4;
3028
- const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3029
- const iColItems = 8;
3030
- const iColVal = 16;
3031
- const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3032
-
3033
- // Print a unique marker, query its position, then overwrite it with the table
3034
- // Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
3035
- let invBaseRow = 1;
3036
- const captureRow = () => new Promise(resolve => {
3037
- const chunks = [];
3038
- const handler = (chunk) => {
3039
- chunks.push(chunk);
3040
- const raw = chunks.join('');
3041
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3042
- if (m) {
3043
- process.stdin.removeListener('data', handler);
3044
- invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
3045
- resolve();
3046
- }
3047
- };
3048
- process.stdin.on('data', handler);
3049
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3050
- process.stderr.write(MARKER);
3051
- setTimeout(resolve, 50);
3052
- });
3053
- await captureRow();
3054
-
3055
- // Now print the inventory table starting at invBaseRow
3056
- const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3057
- console.log(` ${'─'.repeat(invVis)}`);
3058
- for (let i = 0; i < activeWorkers.length; i++) {
3059
- const w = activeWorkers[i];
3060
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3061
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3062
- console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'···'.padEnd(iColVal)}${c.reset}`);
3063
- }
3064
- console.log(` ${'─'.repeat(invVis)}`);
3065
-
3066
- let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3067
- const drawInvProgress = () => {
3068
- if (invPending === 0) return;
3069
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3070
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3071
- const barW = Math.min(20, startupTw - 40);
3072
- const filled = Math.round(pct * barW);
3073
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3074
- const pctStr = `${Math.round(pct * 100)}%`;
3075
- invMoveToRow(invBaseRow);
3076
- 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`);
3077
- };
3078
- const invSpinnerInterval = setInterval(drawInvProgress, 80);
3079
-
3080
- await Promise.all(activeWorkers.map(async (w, i) => {
3081
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3082
- const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3083
- let invRes;
3084
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3085
- catch { invRes = { ok: false }; }
3086
- invPending--;
3087
- const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3088
- const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3089
- const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3090
- const itemStr = `${items}`.padEnd(iColItems);
3091
- const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3092
- const row = invBaseRow + 1 + i;
3093
- invMoveToRow(row);
3094
- process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3095
- 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++; }
3096
2814
  }));
3097
-
3098
- clearInterval(invSpinnerInterval);
3099
- process.stdout.write(`\r\x1b[2K`);
3100
-
3101
2815
  if (invFailed > 0) {
3102
- 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}`);
3103
- 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.`);
3104
2817
  return;
3105
2818
  }
3106
- 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}`);
3107
- console.log('');
3108
-
3109
- // ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
3110
- const bColNum = 4;
3111
- const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3112
- const bColWallet = 12;
3113
- const bColBank = 12;
3114
- const bColTotal = 14;
3115
- const bColLs = 4;
3116
- const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3117
-
3118
- // Capture starting row for balance phase
3119
- // Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
3120
- let balBaseRow = 1;
3121
- const balCaptureRow = () => new Promise(resolve => {
3122
- const chunks = [];
3123
- const handler = (chunk) => {
3124
- chunks.push(chunk);
3125
- const raw = chunks.join('');
3126
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3127
- if (m) {
3128
- process.stdin.removeListener('data', handler);
3129
- balBaseRow = parseInt(m[1], 10) + 1;
3130
- resolve();
3131
- }
3132
- };
3133
- process.stdin.on('data', handler);
3134
- // Write to stderr so PTY doesn't echo the visible MARKER text to stdout
3135
- process.stderr.write(MARKER);
3136
- setTimeout(resolve, 50);
3137
- });
3138
- await balCaptureRow();
3139
-
3140
- const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3141
- console.log(` ${'─'.repeat(balVis)}`);
3142
- for (let i = 0; i < activeWorkers.length; i++) {
3143
- const w = activeWorkers[i];
3144
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3145
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3146
- 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}`);
3147
- }
3148
- console.log(` ${'─'.repeat(balVis)}`);
3149
-
3150
- let balDone = 0, balPending = activeWorkers.length;
3151
- const drawBalProgress = () => {
3152
- if (balPending === 0) return;
3153
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3154
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3155
- const barW = Math.min(20, startupTw - 40);
3156
- const filled = Math.round(pct * barW);
3157
- const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3158
- balMoveToRow(balBaseRow);
3159
- 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`);
3160
- };
3161
- const balSpinnerInterval = setInterval(drawBalProgress, 80);
3162
-
3163
- await Promise.all(activeWorkers.map(async (w, i) => {
3164
- try { await w.checkBalance(true); } catch {}
3165
- balPending--;
3166
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3167
- const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3168
- const wallet = w.stats?.balance || 0;
3169
- const bank = w.stats?.bankBalance || 0;
3170
- const ls = w._lifesavers ?? '?';
3171
- const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3172
- const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3173
- const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3174
- const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3175
- const row = balBaseRow + 1 + i;
3176
- balMoveToRow(row);
3177
- 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`);
3178
- balDone++;
3179
- }));
3180
-
3181
- clearInterval(balSpinnerInterval);
3182
- process.stdout.write(`\r\x1b[2K`);
2819
+ console.log('Inventory check complete');
3183
2820
 
3184
- let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
2821
+ // ── Phase 2.5: Balance check ───────────────────────────────────
2822
+ console.log('Checking balances...');
3185
2823
  for (const w of activeWorkers) {
3186
- totalWallet += w.stats?.balance || 0;
3187
- totalBank += w.stats?.bankBalance || 0;
3188
- if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
2824
+ try { await w.checkBalance(true); } catch {}
3189
2825
  }
3190
- 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}`);
3191
- if (noLifesaverAccounts.length > 0) {
3192
- 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;
3193
2830
  }
3194
- console.log('');
3195
-
2831
+ console.log(`Balances: total ${totalCoins.toLocaleString()} coins across ${activeWorkers.length} accounts`);
3196
2832
 
3197
- // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3198
- const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3199
- console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3200
- 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 = [];
3201
2836
  for (const w of activeWorkers) {
3202
2837
  try {
3203
2838
  const dm = await w.checkDmHistory();
3204
2839
  if (dm.deaths > 0) dmDeaths += dm.deaths;
3205
2840
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3206
2841
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
3207
- if (dm.lifesavers === -1) dmUnknown.push(w.username);
3208
- // Store level and lifesaver for dashboard
3209
2842
  if (dm.currentLevel > 0) w._level = dm.currentLevel;
3210
2843
  if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
3211
- const parts = [];
3212
- if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
3213
- if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
3214
- if (dm.lifesavers >= 0) {
3215
- const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3216
- parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
3217
- } else {
3218
- // Unknown lifesavers — pulse to show pending
3219
- const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
3220
- parts.push(`${D}${pulse}♥?${c.reset}`);
3221
- }
3222
- } catch {}
3223
- }
3224
- if (dmNoLs.length > 0) {
3225
- log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
3226
- // Set Redis keys to block crime/search
3227
- for (const w of activeWorkers) {
3228
- if (dmNoLs.includes(w.username) && redis) {
2844
+ // Block crime/search for accounts with 0 lifesavers
2845
+ if (dm.lifesavers === 0 && redis) {
3229
2846
  try {
3230
2847
  await redis.set(`dkg:lifesavers:${w.account.id}`, '0', 'EX', 86400);
3231
- await redis.set(`raw:alert:no-lifesaver:${w.channel?.id}`, '1', 'EX', 86400);
3232
2848
  } catch {}
3233
2849
  }
3234
- }
3235
- }
3236
- if (dmUnknown.length > 0) {
3237
- log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
3238
- // Crime/search on these accounts will be skipped via safety hold until the live
3239
- // DM gateway listener detects a death (→ sets count) or confirms clean.
2850
+ } catch {}
3240
2851
  }
3241
- const dmSummaryParts = [];
3242
- if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
3243
- if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
3244
- if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
3245
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups'}`);
3246
- 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'}`);
3247
2857
 
3248
- console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
3249
-
3250
- // Phase 3: Start all grind loops (only for valid workers)
2858
+ // ── Phase 3: Start grind loops ───────────────────────────────────
2859
+ console.log(`Starting ${activeWorkers.length} grind loops...`);
3251
2860
  for (const w of activeWorkers) {
3252
2861
  if (!shutdownCalled) w.grindLoop();
3253
2862
  }
3254
-
3255
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
3256
- console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
2863
+ console.log(`All grind loops started. v${PKG_VERSION} | Ctrl+C to stop`);
3257
2864
 
3258
2865
  // Cluster heartbeat — lets other nodes see this node is alive
3259
2866
  if (CLUSTER_ENABLED) {
@@ -3322,31 +2929,23 @@ async function start(apiKey, apiUrl, opts = {}) {
3322
2929
  setDashboardActive(false);
3323
2930
  process.stdout.write(c.show);
3324
2931
 
3325
- const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
3326
2932
  console.log('');
3327
- console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
3328
- console.log(sepBar);
2933
+ console.log('Session Summary');
3329
2934
 
3330
2935
  // Collect stats from all workers (including rotated-out ones)
3331
2936
  let finalCoins = 0;
3332
2937
  let finalCmds = 0;
3333
2938
  for (const wk of workers) {
3334
2939
  const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
3335
- console.log(
3336
- ` ${wk.color}${c.bold}${(wk.username || '?').padEnd(18)}${c.reset}` +
3337
- ` ${rgb(52, 211, 153)}+⏣ ${wk.stats.coins.toLocaleString().padStart(8)}${c.reset}` +
3338
- ` ${c.dim}${wk.stats.commands.toString().padStart(4)} cmds${c.reset}` +
3339
- ` ${c.dim}${rate}% success${c.reset}`
3340
- );
2940
+ console.log(` ${(wk.username || '?').padEnd(18)} +${wk.stats.coins.toLocaleString().padStart(8)} coins ${wk.stats.commands} cmds ${rate}% ok`);
3341
2941
  finalCoins += wk.stats.coins || 0;
3342
2942
  finalCmds += wk.stats.commands || 0;
3343
2943
  }
3344
- console.log(sepBar);
3345
2944
 
3346
2945
  const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
3347
2946
  const avgEarn = globalEarningsEMA.get();
3348
2947
  const cpm = globalCmdRate.getRate().toFixed(1);
3349
- 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`);
3350
2949
  console.log('');
3351
2950
 
3352
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.15.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"