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.
- package/lib/grinder.js +59 -404
- 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
|
|
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 =
|
|
2696
|
-
const bar = '─'.repeat(tw);
|
|
2649
|
+
const tw = 80;
|
|
2697
2650
|
|
|
2698
|
-
|
|
2699
|
-
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
2705
|
+
console.log(`Cluster: claimed ${accounts.length}/${totalBefore} accounts`);
|
|
2765
2706
|
if (accounts.length === 0) {
|
|
2766
|
-
log('
|
|
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
|
|
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
|
-
|
|
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(
|
|
2957
|
-
console.log('');
|
|
2795
|
+
console.log(`Login complete: ${loginDone}/${accounts.length} connected`);
|
|
2958
2796
|
if (invalidWorkers.length > 0) {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
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
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
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(`
|
|
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(
|
|
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
|
-
|
|
3105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
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
|
-
|
|
3139
|
-
|
|
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
|
-
|
|
3153
|
-
if (dm.
|
|
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
|
-
|
|
3183
|
-
|
|
3184
|
-
if (
|
|
3185
|
-
if (
|
|
3186
|
-
console.log(`
|
|
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
|
-
|
|
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(
|
|
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(`
|
|
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
|