dankgrinder 7.83.0 → 8.1.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/dashboard.js +269 -4
- package/lib/grinder.js +617 -157
- package/lib/rawLogger.js +2 -12
- package/package.json +1 -1
- package/lib/terminal.js +0 -720
package/lib/grinder.js
CHANGED
|
@@ -3,7 +3,7 @@ const Redis = require('ioredis');
|
|
|
3
3
|
const commands = require('./commands');
|
|
4
4
|
const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
|
|
5
5
|
const rawLogger = require('./rawLogger');
|
|
6
|
-
const
|
|
6
|
+
const { renderDashboard: renderDashboardImpl } = require('./dashboard');
|
|
7
7
|
const {
|
|
8
8
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
9
9
|
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
|
|
@@ -95,20 +95,10 @@ const c = {
|
|
|
95
95
|
};
|
|
96
96
|
|
|
97
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@@';
|
|
98
100
|
const DANK_MEMER_ID = '270904126974590976';
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
// Simple uptime formatter
|
|
102
|
-
function formatUptime() {
|
|
103
|
-
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
104
|
-
if (s < 60) return `${s}s`;
|
|
105
|
-
const m = Math.floor(s / 60);
|
|
106
|
-
if (m < 60) return `${m}m ${s % 60}s`;
|
|
107
|
-
const h = Math.floor(m / 60);
|
|
108
|
-
if (h < 24) return `${h}h ${m % 60}m`;
|
|
109
|
-
const d = Math.floor(h / 24);
|
|
110
|
-
return `${d}d ${h % 24}h`;
|
|
111
|
-
}
|
|
112
102
|
// ── Safe options for search/crime ──────────────────────────
|
|
113
103
|
// Object.freeze → V8 marks these as immutable, enabling inline caching
|
|
114
104
|
// and preventing accidental mutation across 10K worker instances.
|
|
@@ -130,8 +120,6 @@ let API_URL = '';
|
|
|
130
120
|
let REDIS_URL = process.env.REDIS_URL || '';
|
|
131
121
|
let redis = null;
|
|
132
122
|
let workers = [];
|
|
133
|
-
let startTime = 0;
|
|
134
|
-
let shutdownCalled = false;
|
|
135
123
|
|
|
136
124
|
// ── Cluster Mode Config ──────────────────────────────────────
|
|
137
125
|
// NODE_ID uniquely identifies this process in a multi-node cluster.
|
|
@@ -145,25 +133,10 @@ const CLUSTER_PREFIX = 'dkg:cluster:';
|
|
|
145
133
|
function initRedis() {
|
|
146
134
|
if (!redis && REDIS_URL) {
|
|
147
135
|
try {
|
|
148
|
-
redis = new Redis(REDIS_URL, {
|
|
149
|
-
|
|
150
|
-
retryStrategy: (times) => times > 5 ? null : Math.min(times * 500, 3000),
|
|
151
|
-
lazyConnect: true,
|
|
152
|
-
});
|
|
153
|
-
redis.connect().catch((e) => {
|
|
154
|
-
// Only warn once — don't spam on persistent connection failures
|
|
155
|
-
if (!redis || redis.status === 'wait') {
|
|
156
|
-
console.warn(`[Redis] connection failed: ${e.message} — continuing without Redis`);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
redis.on('error', (e) => {
|
|
160
|
-
// Suppress common transient errors from spamming stderr
|
|
161
|
-
const msg = e?.message || '';
|
|
162
|
-
if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('connect')) return;
|
|
163
|
-
console.error(`[Redis] error: ${msg}`);
|
|
164
|
-
});
|
|
136
|
+
redis = new Redis(REDIS_URL, { maxRetriesPerRequest: null, lazyConnect: true });
|
|
137
|
+
redis.connect().catch(() => {});
|
|
165
138
|
} catch (e) {
|
|
166
|
-
|
|
139
|
+
console.error('Redis connection failed', e);
|
|
167
140
|
}
|
|
168
141
|
}
|
|
169
142
|
}
|
|
@@ -285,6 +258,17 @@ function progressBar(value, max, width, filledColor, emptyColor) {
|
|
|
285
258
|
return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
|
|
286
259
|
}
|
|
287
260
|
|
|
261
|
+
// ── Animated braille spinner frames ──────────────────────────
|
|
262
|
+
const BRAILLE_SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
263
|
+
const BLOCK_SPIN = ['▉', '▊', '▋', '▌', '▍', '▎', '▏', '▎', '▍', '▌', '▋', '▊'];
|
|
264
|
+
const PULSE_CHARS = ['○', '◎', '●', '◉', '●', '◎'];
|
|
265
|
+
function getSpinner(type = 'braille') {
|
|
266
|
+
const now = Math.floor(Date.now() / 80);
|
|
267
|
+
if (type === 'block') return BLOCK_SPIN[now % BLOCK_SPIN.length];
|
|
268
|
+
if (type === 'pulse') return PULSE_CHARS[now % PULSE_CHARS.length];
|
|
269
|
+
return BRAILLE_SPIN[now % BRAILLE_SPIN.length];
|
|
270
|
+
}
|
|
271
|
+
|
|
288
272
|
// ── Box drawing helpers ──────────────────────────────────────
|
|
289
273
|
const BOX = {
|
|
290
274
|
tl: '╭', tr: '╮', bl: '╰', br: '╯',
|
|
@@ -337,28 +321,123 @@ function colorBanner() {
|
|
|
337
321
|
return out;
|
|
338
322
|
}
|
|
339
323
|
|
|
340
|
-
// ──
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
324
|
+
// ── Live Dashboard State ─────────────────────────────────────
|
|
325
|
+
let dashboardLines = 0;
|
|
326
|
+
let dashboardStarted = false;
|
|
327
|
+
let dashboardRendering = false;
|
|
328
|
+
let lastRenderTime = 0;
|
|
329
|
+
let renderPending = false;
|
|
330
|
+
let totalBalance = 0;
|
|
331
|
+
let totalCoins = 0;
|
|
332
|
+
let totalCommands = 0;
|
|
333
|
+
let startTime = Date.now();
|
|
334
|
+
let shutdownCalled = false;
|
|
335
|
+
let sessionPeakCoins = 0;
|
|
336
|
+
let isNewHigh = false;
|
|
337
|
+
// RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
|
|
338
|
+
const recentLogs = new RingBuffer(8);
|
|
339
|
+
const MAX_LOGS = 8;
|
|
340
|
+
const RENDER_THROTTLE_MS = 200;
|
|
341
|
+
// Earnings history for sparkline (sample every 10 seconds)
|
|
342
|
+
const earningsHistory = new RingBuffer(30);
|
|
343
|
+
let lastEarningsSample = 0;
|
|
344
|
+
// Per-command stats tracking
|
|
345
|
+
const cmdStats = new Map();
|
|
346
|
+
// Coins per minute history for rate graph
|
|
347
|
+
const cpmHistory = new RingBuffer(20);
|
|
348
|
+
let lastCpmSample = 0;
|
|
349
|
+
|
|
350
|
+
function formatUptime() {
|
|
351
|
+
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
352
|
+
const h = Math.floor(s / 3600);
|
|
353
|
+
const m = Math.floor((s % 3600) / 60);
|
|
354
|
+
const sec = s % 60;
|
|
355
|
+
if (h > 0) return `${h}h ${m}m ${sec}s`;
|
|
356
|
+
if (m > 0) return `${m}m ${sec}s`;
|
|
357
|
+
return `${sec}s`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatCoins(n) {
|
|
361
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
362
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
363
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
364
|
+
return n.toLocaleString();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function scheduleRender() {
|
|
368
|
+
if (renderPending || !dashboardStarted) return;
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const elapsed = now - lastRenderTime;
|
|
371
|
+
if (elapsed >= RENDER_THROTTLE_MS) {
|
|
372
|
+
renderDashboard();
|
|
373
|
+
} else {
|
|
374
|
+
renderPending = true;
|
|
375
|
+
setTimeout(() => { renderPending = false; renderDashboard(); }, RENDER_THROTTLE_MS - elapsed);
|
|
351
376
|
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Dashboard ──────────────────────────────────────────────────────────────────
|
|
380
|
+
// Thin wrapper: aggregates stats then delegates to ./dashboard.js
|
|
381
|
+
function renderDashboard() {
|
|
382
|
+
if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
|
|
383
|
+
dashboardRendering = true;
|
|
384
|
+
lastRenderTime = Date.now();
|
|
385
|
+
|
|
386
|
+
// Aggregate session totals
|
|
387
|
+
totalBalance = 0; totalCoins = 0; totalCommands = 0;
|
|
388
|
+
let totalErrors = 0;
|
|
389
|
+
for (const w of workers) {
|
|
390
|
+
totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
391
|
+
totalCoins += w.stats.coins || 0;
|
|
392
|
+
totalCommands += w.stats.commands || 0;
|
|
393
|
+
totalErrors += w.stats.errors || 0;
|
|
394
|
+
}
|
|
395
|
+
if (totalCoins > sessionPeakCoins) {
|
|
396
|
+
sessionPeakCoins = totalCoins;
|
|
397
|
+
isNewHigh = true;
|
|
398
|
+
setTimeout(() => { isNewHigh = false; }, 3000);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Pass all state into the dashboard module
|
|
402
|
+
const newLines = renderDashboardImpl({
|
|
403
|
+
workers, dashboardStarted, dashboardRendering, dashboardLines,
|
|
404
|
+
totalBalance, totalCoins, totalCommands, startTime,
|
|
405
|
+
sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
|
|
406
|
+
earningsHistory, lastEarningsSample,
|
|
407
|
+
CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
|
|
408
|
+
AccountWorker, PULSE_CHARS, getSpinner, gradientText,
|
|
409
|
+
rgb, c, BOX,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (newLines != null) dashboardLines = Math.max(dashboardLines, newLines);
|
|
413
|
+
dashboardRendering = false;
|
|
414
|
+
}
|
|
352
415
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
debug: `${c.dim}·${c.reset}`,
|
|
416
|
+
function log(type, msg, label) {
|
|
417
|
+
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
418
|
+
const icons = {
|
|
419
|
+
info: '·', success: '✓', error: '✗', warn: '!',
|
|
420
|
+
cmd: '▸', coin: '$', buy: '♦', bal: '◈', debug: '·',
|
|
359
421
|
};
|
|
360
|
-
const
|
|
361
|
-
|
|
422
|
+
const tagRaw = label ? label.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 12) : '';
|
|
423
|
+
const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
|
|
424
|
+
const tw = Math.max(process.stdout.columns || 80, 60);
|
|
425
|
+
if (dashboardStarted) {
|
|
426
|
+
const maxLen = tw - 8;
|
|
427
|
+
const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
|
|
428
|
+
recentLogs.push(entry.substring(0, maxLen));
|
|
429
|
+
scheduleRender();
|
|
430
|
+
} else {
|
|
431
|
+
const colorIcons = {
|
|
432
|
+
info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
|
|
433
|
+
error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
|
|
434
|
+
cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
|
|
435
|
+
buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
|
|
436
|
+
debug: `${c.dim}·${c.reset}`,
|
|
437
|
+
};
|
|
438
|
+
const tagCol = label ? `${label} ` : '';
|
|
439
|
+
console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
|
|
440
|
+
}
|
|
362
441
|
}
|
|
363
442
|
|
|
364
443
|
async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
|
|
@@ -790,6 +869,7 @@ class AccountWorker {
|
|
|
790
869
|
|
|
791
870
|
setStatus(text) {
|
|
792
871
|
this.lastStatus = stripAnsi(String(text || '')).replace(/\s+/g, ' ').trim();
|
|
872
|
+
if (dashboardStarted) scheduleRender();
|
|
793
873
|
}
|
|
794
874
|
|
|
795
875
|
waitForDankMemer(timeout = 15000) {
|
|
@@ -1402,32 +1482,34 @@ class AccountWorker {
|
|
|
1402
1482
|
|
|
1403
1483
|
// Update Redis with findings
|
|
1404
1484
|
if (redis) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1485
|
+
if (currentLevel > 0) {
|
|
1486
|
+
await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
|
|
1487
|
+
this._level = currentLevel;
|
|
1488
|
+
// Only log to terminal during startup — after dashboardStarted, go to live feed
|
|
1489
|
+
if (dashboardStarted) this.log('info', `DM level: ${c.bold}${currentLevel}${c.reset}`);
|
|
1490
|
+
}
|
|
1491
|
+
if (lastLifesaverCount >= 0) {
|
|
1492
|
+
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
|
|
1493
|
+
this._lifesavers = lastLifesaverCount;
|
|
1494
|
+
if (lastLifesaverCount === 0) {
|
|
1495
|
+
await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
|
|
1496
|
+
await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
|
|
1497
|
+
if (dashboardStarted) this.log('error', `${c.red}0 LIFESAVERS! Crime/Search will be disabled.${c.reset}`);
|
|
1417
1498
|
}
|
|
1418
|
-
}
|
|
1499
|
+
}
|
|
1419
1500
|
}
|
|
1420
1501
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1502
|
+
return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
|
|
1503
|
+
} catch (e) {
|
|
1504
|
+
lastError = e;
|
|
1505
|
+
if (attempt < maxRetries - 1) {
|
|
1506
|
+
await new Promise(r => setTimeout(r, delays[attempt]));
|
|
1507
|
+
}
|
|
1426
1508
|
}
|
|
1427
1509
|
}
|
|
1510
|
+
if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
1511
|
+
return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
|
|
1428
1512
|
}
|
|
1429
|
-
return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
|
|
1430
|
-
}
|
|
1431
1513
|
|
|
1432
1514
|
// ── Run Single Command ──────────────────────────────────────
|
|
1433
1515
|
// Each modular command handler sends the command, waits for response,
|
|
@@ -1635,8 +1717,6 @@ class AccountWorker {
|
|
|
1635
1717
|
// ── Death / lifesaver detection in command responses ──
|
|
1636
1718
|
if (resultLower.includes('you died') || resultLower.includes('lifesaver protected')) {
|
|
1637
1719
|
this.log('error', `DEATH DETECTED during ${cmdName}!`);
|
|
1638
|
-
terminal.flashEvent('death', `💀 ${this.username} died — ${lsCount === 0 ? '0 lifesavers!' : (lsCount > 0 ? `${lsCount} lifesavers left` : 'check DM')}`);
|
|
1639
|
-
terminal.markWorkerDirty(this.idx);
|
|
1640
1720
|
// Check for lifesaver count in the response
|
|
1641
1721
|
const lsMatch = result.match(/(\d+)\s*life\s*saver/i);
|
|
1642
1722
|
const lsCount = lsMatch ? parseInt(lsMatch[1]) : -1;
|
|
@@ -1653,8 +1733,6 @@ class AccountWorker {
|
|
|
1653
1733
|
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
|
|
1654
1734
|
this.log('warn', `Lifesaver used! ${lsCount} remaining.`);
|
|
1655
1735
|
if (lsCount <= 2) {
|
|
1656
|
-
terminal.flashEvent('lowls', `⚠ ${this.username} low lifesavers: ${lsCount} left`);
|
|
1657
|
-
terminal.markWorkerDirty(this.idx);
|
|
1658
1736
|
sendWebhook('LOW LIFESAVERS', `**${this.username}** has only **${lsCount}** lifesaver(s) left!`, 0xfbbf24);
|
|
1659
1737
|
}
|
|
1660
1738
|
}
|
|
@@ -1774,8 +1852,6 @@ class AccountWorker {
|
|
|
1774
1852
|
this.setStatus(formattedResult);
|
|
1775
1853
|
await sendLog(this.username, cmdName, result, 'success');
|
|
1776
1854
|
reportEarnings(this.account.id, this.username, earned, spent, cmdName);
|
|
1777
|
-
terminal.markWorkerDirty(this.idx);
|
|
1778
|
-
if (earned > 0) terminal.flashEvent('success', `⚔ ${this.username} ${cmdName}: +⏣ ${earned.toLocaleString()}`);
|
|
1779
1855
|
|
|
1780
1856
|
// Auto-sell fish every 5 fishing rounds
|
|
1781
1857
|
if (cmdName === 'fish') {
|
|
@@ -2759,23 +2835,34 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2759
2835
|
API_KEY = apiKey;
|
|
2760
2836
|
API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
|
|
2761
2837
|
const CLOUD_MODE = opts.cloud === true;
|
|
2762
|
-
startTime = Date.now();
|
|
2763
2838
|
|
|
2839
|
+
if (CLOUD_MODE) {
|
|
2840
|
+
// In cloud mode, API_KEY is the CLOUD_ADMIN_KEY — not used for user auth.
|
|
2841
|
+
// Per-account keys are fetched per-account from /api/cloud/grinders.
|
|
2842
|
+
console.log('🌥️ Starting in CLOUD MODE — grinding all cloud-enabled accounts');
|
|
2843
|
+
}
|
|
2764
2844
|
REDIS_URL = process.env.REDIS_URL || '';
|
|
2765
2845
|
WEBHOOK_URL = process.env.WEBHOOK_URL || '';
|
|
2766
2846
|
|
|
2847
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
2848
|
+
const tw = Math.min(process.stdout.columns || 80, 78);
|
|
2849
|
+
const bar = c.dim + '─'.repeat(tw) + c.reset;
|
|
2850
|
+
|
|
2767
2851
|
// Detect zlib-sync availability
|
|
2768
2852
|
let hasZlib = false;
|
|
2769
2853
|
try { require('zlib-sync'); hasZlib = true; } catch {}
|
|
2770
2854
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2855
|
+
console.log(colorBanner());
|
|
2856
|
+
console.log(
|
|
2857
|
+
` ${rgb(139, 92, 246)}v${PKG_VERSION}${c.reset}` +
|
|
2858
|
+
` ${c.dim}·${c.reset} ${c.white}${AccountWorker.COMMAND_MAP.length} Commands${c.reset}` +
|
|
2859
|
+
` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}${CLOUD_MODE ? 'Cloud Mode' : (CLUSTER_ENABLED ? 'Cluster Mode' : 'Standalone')}${c.reset}` +
|
|
2860
|
+
` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
|
|
2861
|
+
` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
|
|
2862
|
+
);
|
|
2863
|
+
console.log(bar);
|
|
2774
2864
|
|
|
2775
|
-
|
|
2776
|
-
console.log(`${rgb(139, 92, 246)}🌥️ Starting in CLOUD MODE — grinding all cloud-enabled accounts${c.reset}`);
|
|
2777
|
-
}
|
|
2778
|
-
terminal.startPhase('Fetching accounts...');
|
|
2865
|
+
log('info', `${c.dim}Fetching accounts...${c.reset}`);
|
|
2779
2866
|
|
|
2780
2867
|
const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
|
|
2781
2868
|
let data = await fetchConfig(4, 2000, fetchOpts);
|
|
@@ -2786,11 +2873,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2786
2873
|
data = await fetchConfig(4, 2000, fetchOpts);
|
|
2787
2874
|
}
|
|
2788
2875
|
if (data && data.error) {
|
|
2789
|
-
terminal.endPhase(`API error: ${data.error}`, false);
|
|
2790
2876
|
log('error', `${data.error}`);
|
|
2791
2877
|
return;
|
|
2792
2878
|
}
|
|
2793
|
-
terminal.endPhase(`API connected — ${data.accounts?.length || 0} accounts`);
|
|
2794
2879
|
|
|
2795
2880
|
// Cloud mode: post heartbeat every 30s
|
|
2796
2881
|
if (CLOUD_MODE) {
|
|
@@ -2835,9 +2920,13 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2835
2920
|
}
|
|
2836
2921
|
}
|
|
2837
2922
|
|
|
2923
|
+
const checks = [];
|
|
2924
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
|
|
2925
|
+
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}`);
|
|
2926
|
+
|
|
2838
2927
|
// Init rawLogger Redis (uses same URL — logs all raw gateway data)
|
|
2839
2928
|
if (REDIS_URL) {
|
|
2840
|
-
rawLogger.init(
|
|
2929
|
+
rawLogger.init(REDIS_URL).catch(() => {});
|
|
2841
2930
|
// Listen for DM events across all accounts — update worker state + dashboard LIVE
|
|
2842
2931
|
rawLogger.onDmEvent((event, raw) => {
|
|
2843
2932
|
const channelId = raw.channel_id;
|
|
@@ -2854,8 +2943,6 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2854
2943
|
w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
|
|
2855
2944
|
w.setCooldown?.('crime', 86400);
|
|
2856
2945
|
w.setCooldown?.('search', 86400);
|
|
2857
|
-
terminal.flashEvent('death', `💀 ${w.username} DEATH in DM — 0 lifesavers!`);
|
|
2858
|
-
terminal.markWorkerDirty(w.idx);
|
|
2859
2946
|
sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
2860
2947
|
} else {
|
|
2861
2948
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
@@ -2868,25 +2955,132 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2868
2955
|
}
|
|
2869
2956
|
}
|
|
2870
2957
|
}
|
|
2958
|
+
scheduleRender();
|
|
2871
2959
|
}
|
|
2872
2960
|
|
|
2873
2961
|
if (event.type === 'levelup') {
|
|
2874
2962
|
if (event.to > 0) {
|
|
2875
2963
|
w._level = event.to;
|
|
2876
|
-
|
|
2877
|
-
terminal.markWorkerDirty(w.idx);
|
|
2964
|
+
scheduleRender();
|
|
2878
2965
|
}
|
|
2879
2966
|
}
|
|
2880
2967
|
}
|
|
2881
2968
|
});
|
|
2969
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
|
|
2882
2970
|
}
|
|
2971
|
+
if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
|
|
2972
|
+
if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
|
|
2973
|
+
if (CLUSTER_ENABLED) {
|
|
2974
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
|
|
2975
|
+
}
|
|
2976
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
|
|
2977
|
+
console.log(` ${checks.join(' ')}`);
|
|
2978
|
+
console.log('');
|
|
2979
|
+
|
|
2980
|
+
// ── Phase 1: Login with per-account inline rendering ─────────────────────────
|
|
2981
|
+
const startupTw = process.stdout.columns || 90;
|
|
2982
|
+
const colNum = 4; // " #"
|
|
2983
|
+
const colSts = 3; // "ST"
|
|
2984
|
+
const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
|
|
2985
|
+
const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
|
|
2986
|
+
const colCmds = 8;
|
|
2987
|
+
const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
|
|
2988
|
+
|
|
2989
|
+
const loginStates = accounts.map((acc, i) => ({
|
|
2990
|
+
name: acc.label || acc.id || '?',
|
|
2991
|
+
done: false,
|
|
2992
|
+
failed: false,
|
|
2993
|
+
worker: null,
|
|
2994
|
+
}));
|
|
2883
2995
|
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2996
|
+
let loginLines = [];
|
|
2997
|
+
loginLines.push(` ${'─'.repeat(loginVis)}`);
|
|
2998
|
+
for (let i = 0; i < loginStates.length; i++) {
|
|
2999
|
+
const s = loginStates[i];
|
|
3000
|
+
const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
|
|
3001
|
+
const name = s.name.substring(0, colName).padEnd(colName);
|
|
3002
|
+
const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
|
|
3003
|
+
const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
|
|
3004
|
+
loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
|
|
3005
|
+
}
|
|
3006
|
+
loginLines.push(` ${'─'.repeat(loginVis)}`);
|
|
3007
|
+
for (const l of loginLines) console.log(l);
|
|
3008
|
+
|
|
3009
|
+
// Dynamically capture the starting row of the login table via DSR.
|
|
3010
|
+
// Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
|
|
3011
|
+
// of the visible "@MARKER@@" text portion, which was causing the DSR
|
|
3012
|
+
// response to be swallowed or delayed.
|
|
3013
|
+
let loginBaseRow = 1;
|
|
3014
|
+
const captureLoginRow = () => new Promise(resolve => {
|
|
3015
|
+
const chunks = [];
|
|
3016
|
+
const handler = (chunk) => {
|
|
3017
|
+
chunks.push(chunk);
|
|
3018
|
+
const raw = chunks.join('');
|
|
3019
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3020
|
+
if (m) {
|
|
3021
|
+
process.stdin.removeListener('data', handler);
|
|
3022
|
+
loginBaseRow = parseInt(m[1], 10) + 1;
|
|
3023
|
+
resolve();
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
process.stdin.on('data', handler);
|
|
3027
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3028
|
+
process.stderr.write(MARKER);
|
|
3029
|
+
setTimeout(resolve, 50);
|
|
3030
|
+
});
|
|
3031
|
+
await captureLoginRow();
|
|
3032
|
+
|
|
3033
|
+
let loginPending = new Array(accounts.length).fill(true);
|
|
3034
|
+
const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3035
|
+
|
|
3036
|
+
const drawLoginSpinners = () => {
|
|
3037
|
+
for (let i = 0; i < loginPending.length; i++) {
|
|
3038
|
+
if (!loginPending[i]) continue;
|
|
3039
|
+
const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
3040
|
+
const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
|
|
3041
|
+
const name = loginStates[i].name.substring(0, colName).padEnd(colName);
|
|
3042
|
+
const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
|
|
3043
|
+
const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
|
|
3044
|
+
const row = loginBaseRow + 1 + i; // +1 skips the top border line
|
|
3045
|
+
moveToRow(row);
|
|
3046
|
+
process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
|
|
3047
|
+
}
|
|
3048
|
+
// Move cursor back to bottom to avoid overwriting the bottom border
|
|
3049
|
+
const lastRow = loginBaseRow + 1 + accounts.length + 1;
|
|
3050
|
+
moveToRow(lastRow);
|
|
3051
|
+
};
|
|
3052
|
+
const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
|
|
3053
|
+
|
|
3054
|
+
const finalizeLoginLine = (idx, worker) => {
|
|
3055
|
+
if (!loginPending[idx]) return;
|
|
3056
|
+
loginPending[idx] = false;
|
|
3057
|
+
const s = loginStates[idx];
|
|
3058
|
+
s.done = true;
|
|
3059
|
+
s.worker = worker;
|
|
3060
|
+
|
|
3061
|
+
const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
|
|
3062
|
+
const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
|
|
3063
|
+
let sts, guild, cmds;
|
|
3064
|
+
if (worker._tokenInvalid) {
|
|
3065
|
+
sts = `${rgb(239, 68, 68)}✗${c.reset}`;
|
|
3066
|
+
guild = 'INVALID'.padEnd(colGuild);
|
|
3067
|
+
cmds = '···'.padEnd(colCmds);
|
|
3068
|
+
s.failed = true;
|
|
3069
|
+
} else if (worker.channel) {
|
|
3070
|
+
sts = `${rgb(52, 211, 153)}✓${c.reset}`;
|
|
3071
|
+
const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
|
|
3072
|
+
guild = gn.padEnd(colGuild);
|
|
3073
|
+
cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
|
|
3074
|
+
} else {
|
|
3075
|
+
sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
|
|
3076
|
+
guild = 'timeout'.padEnd(colGuild);
|
|
3077
|
+
cmds = '···'.padEnd(colCmds);
|
|
3078
|
+
}
|
|
3079
|
+
const row = loginBaseRow + 1 + idx; // +1 skips the top border line
|
|
3080
|
+
moveToRow(row);
|
|
3081
|
+
process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
|
|
3082
|
+
};
|
|
2888
3083
|
|
|
2889
|
-
// ── Phase 1: Login accounts ─────────────────────────────────────────
|
|
2890
3084
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
2891
3085
|
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
|
|
2892
3086
|
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
|
|
@@ -2902,70 +3096,203 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2902
3096
|
const worker = new AccountWorker(acc, i + idx);
|
|
2903
3097
|
workers.push(worker);
|
|
2904
3098
|
workerMap.set(acc.id, worker);
|
|
3099
|
+
loginStates[i + idx].worker = worker;
|
|
2905
3100
|
await worker.start();
|
|
2906
|
-
|
|
3101
|
+
finalizeLoginLine(i + idx, worker);
|
|
2907
3102
|
}));
|
|
2908
3103
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2909
3104
|
hintGC();
|
|
2910
3105
|
}
|
|
2911
3106
|
|
|
3107
|
+
clearInterval(loginSpinnerInterval);
|
|
2912
3108
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2913
3109
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2914
3110
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2915
|
-
|
|
3111
|
+
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}`);
|
|
3112
|
+
console.log('');
|
|
2916
3113
|
if (invalidWorkers.length > 0) {
|
|
2917
|
-
|
|
2918
|
-
log('
|
|
2919
|
-
|
|
3114
|
+
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
|
|
3115
|
+
for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
|
|
3116
|
+
console.log('');
|
|
2920
3117
|
}
|
|
2921
|
-
if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login`);
|
|
3118
|
+
if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
|
|
2922
3119
|
|
|
2923
3120
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2924
3121
|
|
|
2925
|
-
// ── Phase 2: Inventory check
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3122
|
+
// ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
|
|
3123
|
+
const iColNum = 4;
|
|
3124
|
+
const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
|
|
3125
|
+
const iColItems = 8;
|
|
3126
|
+
const iColVal = 16;
|
|
3127
|
+
const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
|
|
3128
|
+
|
|
3129
|
+
// Print a unique marker, query its position, then overwrite it with the table
|
|
3130
|
+
// Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
|
|
3131
|
+
let invBaseRow = 1;
|
|
3132
|
+
const captureRow = () => new Promise(resolve => {
|
|
3133
|
+
const chunks = [];
|
|
3134
|
+
const handler = (chunk) => {
|
|
3135
|
+
chunks.push(chunk);
|
|
3136
|
+
const raw = chunks.join('');
|
|
3137
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3138
|
+
if (m) {
|
|
3139
|
+
process.stdin.removeListener('data', handler);
|
|
3140
|
+
invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
|
|
3141
|
+
resolve();
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
3144
|
+
process.stdin.on('data', handler);
|
|
3145
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3146
|
+
process.stderr.write(MARKER);
|
|
3147
|
+
setTimeout(resolve, 50);
|
|
3148
|
+
});
|
|
3149
|
+
await captureRow();
|
|
3150
|
+
|
|
3151
|
+
// Now print the inventory table starting at invBaseRow
|
|
3152
|
+
const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3153
|
+
console.log(` ${'─'.repeat(invVis)}`);
|
|
3154
|
+
for (let i = 0; i < activeWorkers.length; i++) {
|
|
3155
|
+
const w = activeWorkers[i];
|
|
3156
|
+
const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
|
|
3157
|
+
const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
|
|
3158
|
+
console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'···'.padEnd(iColVal)}${c.reset}`);
|
|
3159
|
+
}
|
|
3160
|
+
console.log(` ${'─'.repeat(invVis)}`);
|
|
3161
|
+
|
|
3162
|
+
let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
|
|
3163
|
+
const drawInvProgress = () => {
|
|
3164
|
+
if (invPending === 0) return;
|
|
3165
|
+
const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
3166
|
+
const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
|
|
3167
|
+
const barW = Math.min(20, startupTw - 40);
|
|
3168
|
+
const filled = Math.round(pct * barW);
|
|
3169
|
+
const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
|
|
3170
|
+
const pctStr = `${Math.round(pct * 100)}%`;
|
|
3171
|
+
invMoveToRow(invBaseRow);
|
|
3172
|
+
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`);
|
|
3173
|
+
};
|
|
3174
|
+
const invSpinnerInterval = setInterval(drawInvProgress, 80);
|
|
3175
|
+
|
|
3176
|
+
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
3177
|
+
const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
|
|
3178
|
+
const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
|
|
3179
|
+
let invRes;
|
|
3180
|
+
try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
3181
|
+
catch { invRes = { ok: false }; }
|
|
3182
|
+
invPending--;
|
|
3183
|
+
const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
|
|
3184
|
+
const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
|
|
3185
|
+
const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
|
|
3186
|
+
const itemStr = `${items}`.padEnd(iColItems);
|
|
3187
|
+
const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
|
|
3188
|
+
const row = invBaseRow + 1 + i;
|
|
3189
|
+
invMoveToRow(row);
|
|
3190
|
+
process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
|
|
3191
|
+
if (invRes?.ok) invDone++; else invFailed++;
|
|
2934
3192
|
}));
|
|
2935
3193
|
|
|
3194
|
+
clearInterval(invSpinnerInterval);
|
|
3195
|
+
process.stdout.write(`\r\x1b[2K`);
|
|
3196
|
+
|
|
2936
3197
|
if (invFailed > 0) {
|
|
2937
|
-
|
|
2938
|
-
|
|
3198
|
+
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}`);
|
|
3199
|
+
log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
|
|
2939
3200
|
return;
|
|
2940
3201
|
}
|
|
2941
|
-
|
|
3202
|
+
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}`);
|
|
3203
|
+
console.log('');
|
|
3204
|
+
|
|
3205
|
+
// ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
|
|
3206
|
+
const bColNum = 4;
|
|
3207
|
+
const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
|
|
3208
|
+
const bColWallet = 12;
|
|
3209
|
+
const bColBank = 12;
|
|
3210
|
+
const bColTotal = 14;
|
|
3211
|
+
const bColLs = 4;
|
|
3212
|
+
const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
|
|
3213
|
+
|
|
3214
|
+
// Capture starting row for balance phase
|
|
3215
|
+
// Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
|
|
3216
|
+
let balBaseRow = 1;
|
|
3217
|
+
const balCaptureRow = () => new Promise(resolve => {
|
|
3218
|
+
const chunks = [];
|
|
3219
|
+
const handler = (chunk) => {
|
|
3220
|
+
chunks.push(chunk);
|
|
3221
|
+
const raw = chunks.join('');
|
|
3222
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3223
|
+
if (m) {
|
|
3224
|
+
process.stdin.removeListener('data', handler);
|
|
3225
|
+
balBaseRow = parseInt(m[1], 10) + 1;
|
|
3226
|
+
resolve();
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
process.stdin.on('data', handler);
|
|
3230
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3231
|
+
process.stderr.write(MARKER);
|
|
3232
|
+
setTimeout(resolve, 50);
|
|
3233
|
+
});
|
|
3234
|
+
await balCaptureRow();
|
|
3235
|
+
|
|
3236
|
+
const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3237
|
+
console.log(` ${'─'.repeat(balVis)}`);
|
|
3238
|
+
for (let i = 0; i < activeWorkers.length; i++) {
|
|
3239
|
+
const w = activeWorkers[i];
|
|
3240
|
+
const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
|
|
3241
|
+
const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
|
|
3242
|
+
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}`);
|
|
3243
|
+
}
|
|
3244
|
+
console.log(` ${'─'.repeat(balVis)}`);
|
|
3245
|
+
|
|
3246
|
+
let balDone = 0, balPending = activeWorkers.length;
|
|
3247
|
+
const drawBalProgress = () => {
|
|
3248
|
+
if (balPending === 0) return;
|
|
3249
|
+
const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
3250
|
+
const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
|
|
3251
|
+
const barW = Math.min(20, startupTw - 40);
|
|
3252
|
+
const filled = Math.round(pct * barW);
|
|
3253
|
+
const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
|
|
3254
|
+
balMoveToRow(balBaseRow);
|
|
3255
|
+
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`);
|
|
3256
|
+
};
|
|
3257
|
+
const balSpinnerInterval = setInterval(drawBalProgress, 80);
|
|
2942
3258
|
|
|
2943
|
-
|
|
2944
|
-
terminal.startPhase('Checking balance');
|
|
2945
|
-
terminal.updateProgress(0, activeWorkers.length);
|
|
2946
|
-
let balDone = 0;
|
|
2947
|
-
await Promise.all(activeWorkers.map(async (w) => {
|
|
3259
|
+
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
2948
3260
|
try { await w.checkBalance(true); } catch {}
|
|
3261
|
+
balPending--;
|
|
3262
|
+
const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
|
|
3263
|
+
const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
|
|
3264
|
+
const wallet = w.stats?.balance || 0;
|
|
3265
|
+
const bank = w.stats?.bankBalance || 0;
|
|
3266
|
+
const ls = w._lifesavers ?? '?';
|
|
3267
|
+
const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
3268
|
+
const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
|
|
3269
|
+
const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
|
|
3270
|
+
const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
|
|
3271
|
+
const row = balBaseRow + 1 + i;
|
|
3272
|
+
balMoveToRow(row);
|
|
3273
|
+
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`);
|
|
2949
3274
|
balDone++;
|
|
2950
|
-
terminal.updateProgress(balDone, activeWorkers.length);
|
|
2951
3275
|
}));
|
|
2952
3276
|
|
|
3277
|
+
clearInterval(balSpinnerInterval);
|
|
3278
|
+
process.stdout.write(`\r\x1b[2K`);
|
|
3279
|
+
|
|
2953
3280
|
let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
|
|
2954
3281
|
for (const w of activeWorkers) {
|
|
2955
3282
|
totalWallet += w.stats?.balance || 0;
|
|
2956
3283
|
totalBank += w.stats?.bankBalance || 0;
|
|
2957
3284
|
if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
|
|
2958
3285
|
}
|
|
2959
|
-
|
|
3286
|
+
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}`);
|
|
2960
3287
|
if (noLifesaverAccounts.length > 0) {
|
|
2961
|
-
|
|
2962
|
-
console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
|
|
3288
|
+
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(', ')}`);
|
|
2963
3289
|
}
|
|
3290
|
+
console.log('');
|
|
3291
|
+
|
|
2964
3292
|
|
|
2965
|
-
//
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
let dmDone = 0;
|
|
3293
|
+
// Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
|
|
3294
|
+
const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
3295
|
+
console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
|
|
2969
3296
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
2970
3297
|
for (const w of activeWorkers) {
|
|
2971
3298
|
try {
|
|
@@ -2974,15 +3301,28 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2974
3301
|
if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
|
|
2975
3302
|
if (dm.lifesavers === 0) dmNoLs.push(w.username);
|
|
2976
3303
|
if (dm.lifesavers === -1) dmUnknown.push(w.username);
|
|
3304
|
+
// Store level and lifesaver for dashboard
|
|
2977
3305
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
2978
3306
|
if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
|
|
3307
|
+
const parts = [];
|
|
3308
|
+
if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
|
|
3309
|
+
if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
|
|
3310
|
+
if (dm.lifesavers >= 0) {
|
|
3311
|
+
const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
3312
|
+
parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
|
|
3313
|
+
} else {
|
|
3314
|
+
// Unknown lifesavers — pulse to show pending
|
|
3315
|
+
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
3316
|
+
parts.push(`${D}${pulse}♥?${c.reset}`);
|
|
3317
|
+
}
|
|
3318
|
+
if (parts.length > 0) {
|
|
3319
|
+
recentLogs.push({ ts: Date.now(), username: w.username, color: w.color, command: 'dm check', response: parts.join(' '), status: 'ok' });
|
|
3320
|
+
}
|
|
2979
3321
|
} catch {}
|
|
2980
|
-
dmDone++;
|
|
2981
|
-
terminal.updateProgress(dmDone, activeWorkers.length);
|
|
2982
3322
|
}
|
|
2983
3323
|
if (dmNoLs.length > 0) {
|
|
2984
|
-
|
|
2985
|
-
|
|
3324
|
+
recentLogs.push({ ts: Date.now(), username: 'system', color: rgb(239, 68, 68), command: 'dm check', response: `⚠ No lifesavers: ${dmNoLs.join(', ')}`, status: 'warn' });
|
|
3325
|
+
// Set Redis keys to block crime/search
|
|
2986
3326
|
for (const w of activeWorkers) {
|
|
2987
3327
|
if (dmNoLs.includes(w.username) && redis) {
|
|
2988
3328
|
try {
|
|
@@ -2993,22 +3333,45 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2993
3333
|
}
|
|
2994
3334
|
}
|
|
2995
3335
|
if (dmUnknown.length > 0) {
|
|
2996
|
-
|
|
3336
|
+
recentLogs.push({ ts: Date.now(), username: 'system', color: rgb(251, 191, 36), command: 'dm check', response: `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`, status: 'warn' });
|
|
3337
|
+
// Crime/search on these accounts will be skipped via safety hold until the live
|
|
3338
|
+
// DM gateway listener detects a death (→ sets count) or confirms clean.
|
|
2997
3339
|
}
|
|
2998
3340
|
const dmSummaryParts = [];
|
|
2999
3341
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3000
3342
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3001
3343
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3002
|
-
|
|
3344
|
+
recentLogs.push({ ts: Date.now(), username: 'system', color: rgb(52, 211, 153), command: 'dm check', response: dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups', status: 'ok' });
|
|
3345
|
+
console.log('');
|
|
3346
|
+
|
|
3347
|
+
console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
|
|
3348
|
+
|
|
3349
|
+
// DEBUG: activeWorkers confirmed
|
|
3003
3350
|
|
|
3004
|
-
//
|
|
3005
|
-
terminal.startPhase(`🚀 Launching ${activeWorkers.length} grinders!`);
|
|
3351
|
+
// Phase 3: Start all grind loops (only for valid workers)
|
|
3006
3352
|
for (const w of activeWorkers) {
|
|
3007
3353
|
if (!shutdownCalled) w.grindLoop();
|
|
3008
3354
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3355
|
+
|
|
3356
|
+
startTime = Date.now();
|
|
3357
|
+
dashboardStarted = true;
|
|
3358
|
+
setDashboardActive(true);
|
|
3359
|
+
|
|
3360
|
+
// Clear screen and position cursor at top-left before dashboard takes over
|
|
3361
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
3362
|
+
|
|
3363
|
+
// Setup keyboard shortcuts
|
|
3364
|
+
setupKeyboardShortcuts();
|
|
3365
|
+
|
|
3366
|
+
// Re-render on terminal resize so layout adapts to window size
|
|
3367
|
+
process.stdout.on('resize', () => {
|
|
3368
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
3369
|
+
dashboardLines = 0;
|
|
3370
|
+
scheduleRender();
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
setInterval(() => scheduleRender(), 1000);
|
|
3374
|
+
scheduleRender();
|
|
3012
3375
|
|
|
3013
3376
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
3014
3377
|
if (CLUSTER_ENABLED) {
|
|
@@ -3065,7 +3428,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3065
3428
|
const before = workers.length;
|
|
3066
3429
|
// Keep ALL workers visible — never remove from array (user wants to see gaps)
|
|
3067
3430
|
// Only clean up workerMap entries for accounts fully removed from API
|
|
3068
|
-
if (workers.length !== before)
|
|
3431
|
+
if (workers.length !== before) scheduleRender();
|
|
3069
3432
|
} catch {}
|
|
3070
3433
|
}, 10_000);
|
|
3071
3434
|
|
|
@@ -3074,27 +3437,44 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3074
3437
|
if (sigintHandled) return;
|
|
3075
3438
|
sigintHandled = true;
|
|
3076
3439
|
shutdownCalled = true;
|
|
3440
|
+
dashboardStarted = false;
|
|
3077
3441
|
setDashboardActive(false);
|
|
3442
|
+
process.stdout.write(c.show);
|
|
3078
3443
|
|
|
3079
|
-
|
|
3080
|
-
|
|
3444
|
+
if (dashboardLines > 0) {
|
|
3445
|
+
process.stdout.write(c.cursorUp(dashboardLines));
|
|
3446
|
+
for (let i = 0; i < dashboardLines; i++) {
|
|
3447
|
+
process.stdout.write(c.clearLine + '\r\n');
|
|
3448
|
+
}
|
|
3449
|
+
process.stdout.write(c.cursorUp(dashboardLines));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
|
|
3453
|
+
console.log('');
|
|
3454
|
+
console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
|
|
3455
|
+
console.log(sepBar);
|
|
3456
|
+
|
|
3457
|
+
// Collect stats from all workers (including rotated-out ones)
|
|
3458
|
+
let finalCoins = 0;
|
|
3459
|
+
let finalCmds = 0;
|
|
3081
3460
|
for (const wk of workers) {
|
|
3461
|
+
const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
|
|
3462
|
+
console.log(
|
|
3463
|
+
` ${wk.color}${c.bold}${(wk.username || '?').padEnd(18)}${c.reset}` +
|
|
3464
|
+
` ${rgb(52, 211, 153)}+⏣ ${wk.stats.coins.toLocaleString().padStart(8)}${c.reset}` +
|
|
3465
|
+
` ${c.dim}${wk.stats.commands.toString().padStart(4)} cmds${c.reset}` +
|
|
3466
|
+
` ${c.dim}${rate}% success${c.reset}`
|
|
3467
|
+
);
|
|
3082
3468
|
finalCoins += wk.stats.coins || 0;
|
|
3083
3469
|
finalCmds += wk.stats.commands || 0;
|
|
3084
|
-
totalSuccess += wk.stats.successes || 0;
|
|
3085
3470
|
}
|
|
3471
|
+
console.log(sepBar);
|
|
3472
|
+
|
|
3086
3473
|
const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
totalCoins: finalCoins,
|
|
3092
|
-
totalCmds: finalCmds,
|
|
3093
|
-
totalSuccess: finalCmds > 0 ? ((totalSuccess / finalCmds) * 100).toFixed(0) : 0,
|
|
3094
|
-
workers,
|
|
3095
|
-
uptime: uptimeMs,
|
|
3096
|
-
memMB: memFinal,
|
|
3097
|
-
});
|
|
3474
|
+
const avgEarn = globalEarningsEMA.get();
|
|
3475
|
+
const cpm = globalCmdRate.getRate().toFixed(1);
|
|
3476
|
+
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}`);
|
|
3477
|
+
console.log('');
|
|
3098
3478
|
|
|
3099
3479
|
// Release all cluster claims before stopping workers
|
|
3100
3480
|
const releasePromises = workers.map(wk => releaseClaim(wk.account.id).catch(() => {}));
|
|
@@ -3110,19 +3490,99 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3110
3490
|
|
|
3111
3491
|
const totalRecoveries = workers.reduce((sum, wk) => sum + (wk._totalRecoveries || 0), 0);
|
|
3112
3492
|
const totalDisconnects = workers.reduce((sum, wk) => sum + (wk._disconnectCount || 0), 0);
|
|
3493
|
+
const totalRateLimits = workers.reduce((sum, wk) => sum + (wk._rateLimitHits || 0), 0);
|
|
3113
3494
|
|
|
3114
3495
|
const webhookMsg = `+⏣ ${finalCoins.toLocaleString()} | ${finalCmds} cmds | ${formatUptime()}` +
|
|
3115
3496
|
(totalRecoveries > 0 ? ` | ${totalRecoveries} auto-recoveries` : '') +
|
|
3116
3497
|
(CLUSTER_ENABLED ? ` | node: ${NODE_ID.substring(0, 12)}` : '');
|
|
3117
3498
|
sendWebhook('Session Ended', webhookMsg, 0x8b5cf6);
|
|
3118
3499
|
|
|
3500
|
+
if (totalRecoveries > 0 || totalDisconnects > 0) {
|
|
3501
|
+
console.log(` ${c.dim}Recovery stats: ${totalRecoveries} auto-recoveries, ${totalDisconnects} disconnects, ${totalRateLimits} rate limits${c.reset}`);
|
|
3502
|
+
}
|
|
3503
|
+
if (CLUSTER_ENABLED) {
|
|
3504
|
+
console.log(` ${c.dim}Cluster node: ${NODE_ID} — claims released${c.reset}`);
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3119
3507
|
if (redis) { try { redis.disconnect(); } catch {} }
|
|
3120
|
-
setTimeout(() => process.exit(0),
|
|
3508
|
+
setTimeout(() => process.exit(0), 1500);
|
|
3121
3509
|
});
|
|
3122
3510
|
}
|
|
3123
3511
|
|
|
3124
3512
|
// ══════════════════════════════════════════════════════════════
|
|
3125
|
-
// Keyboard
|
|
3513
|
+
// ═ Keyboard Shortcuts (Quality of Life)
|
|
3514
|
+
// ══════════════════════════════════════════════════════════════
|
|
3515
|
+
// Single-key shortcuts for common actions
|
|
3516
|
+
function setupKeyboardShortcuts() {
|
|
3517
|
+
if (process.stdin.isTTY) {
|
|
3518
|
+
process.stdin.setRawMode(true);
|
|
3519
|
+
process.stdin.resume();
|
|
3520
|
+
process.stdin.setEncoding('utf8');
|
|
3521
|
+
|
|
3522
|
+
// Premium styled keyboard shortcuts with gradient box
|
|
3523
|
+
const accent = rgb(139, 92, 246);
|
|
3524
|
+
const dim = c.dim;
|
|
3525
|
+
const kw = 60;
|
|
3526
|
+
console.log('');
|
|
3527
|
+
console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
|
|
3528
|
+
console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
|
|
3529
|
+
console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
|
|
3530
|
+
console.log(` ${accent}│${c.reset} ${rgb(96, 165, 250)}P${c.reset} ${dim}Pause all${c.reset} ${rgb(52, 211, 153)}R${c.reset} ${dim}Resume all${c.reset} ${rgb(251, 191, 36)}S${c.reset} ${dim}Status${c.reset} ${rgb(239, 68, 68)}Q${c.reset} ${dim}Quit${c.reset}${' '.repeat(Math.max(0, kw - 54))}${accent}│${c.reset}`);
|
|
3531
|
+
console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
|
|
3532
|
+
console.log('');
|
|
3533
|
+
|
|
3534
|
+
process.stdin.on('data', (key) => {
|
|
3535
|
+
const k = key.toString().toLowerCase();
|
|
3536
|
+
|
|
3537
|
+
// Ctrl+C or q = quit
|
|
3538
|
+
if (k === '\u0003' || k === 'q') {
|
|
3539
|
+
process.stdout.write(c.show);
|
|
3540
|
+
console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
|
|
3541
|
+
process.emit('SIGINT');
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
// p = pause all accounts
|
|
3546
|
+
if (k === 'p') {
|
|
3547
|
+
let count = 0;
|
|
3548
|
+
workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
|
|
3549
|
+
recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
|
|
3550
|
+
scheduleRender();
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
// r = resume all accounts
|
|
3555
|
+
if (k === 'r') {
|
|
3556
|
+
let count = 0;
|
|
3557
|
+
workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
|
|
3558
|
+
recentLogs.push(`>> RESUMED ${count} accounts`);
|
|
3559
|
+
scheduleRender();
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// s = show status summary (pushed to log feed)
|
|
3564
|
+
if (k === 's') {
|
|
3565
|
+
const active = workers.filter(w => w.running && !w.paused).length;
|
|
3566
|
+
const paused = workers.filter(w => w.paused).length;
|
|
3567
|
+
const invalid = workers.filter(w => w._tokenInvalid).length;
|
|
3568
|
+
const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
|
|
3569
|
+
const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
3570
|
+
const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
|
|
3571
|
+
recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
|
|
3572
|
+
recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
|
|
3573
|
+
scheduleRender();
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// ? or h = show help
|
|
3578
|
+
if (k === '?' || k === 'h') {
|
|
3579
|
+
recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
|
|
3580
|
+
scheduleRender();
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3126
3586
|
|
|
3127
3587
|
// Export the start function for CLI
|
|
3128
3588
|
module.exports = { start };
|