dankgrinder 8.1.0 → 8.2.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 +4 -269
- package/lib/grinder.js +91 -561
- package/lib/rawLogger.js +12 -2
- package/package.json +1 -1
package/lib/dashboard.js
CHANGED
|
@@ -1,271 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Every render writes N rows from line 1 onward. Previous content is overwritten.
|
|
6
|
-
*
|
|
7
|
-
* Context:
|
|
8
|
-
* workers, dashboardStarted, dashboardRendering, dashboardLines,
|
|
9
|
-
* totalBalance, totalCoins, totalCommands, startTime,
|
|
10
|
-
* sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
|
|
11
|
-
* CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
|
|
12
|
-
* AccountWorker, PULSE_CHARS, getSpinner, gradientText,
|
|
13
|
-
* rgb, c
|
|
2
|
+
* CLI Dashboard — REMOVED
|
|
3
|
+
* Dashboard functionality has been removed from the CLI.
|
|
4
|
+
* All startup output goes through simple console.log.
|
|
14
5
|
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
// Local refs (set per render)
|
|
19
|
-
let _c, _rgb, _spinnerFn, _gradientFn;
|
|
20
|
-
|
|
21
|
-
function init(ctx) {
|
|
22
|
-
_c = ctx.c;
|
|
23
|
-
_rgb = ctx.rgb;
|
|
24
|
-
_spinnerFn = ctx.getSpinner;
|
|
25
|
-
_gradientFn = ctx.gradientText;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Helpers ─────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
function vis(s) {
|
|
31
|
-
return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function pad(str, width) {
|
|
35
|
-
return str + ' '.repeat(Math.max(0, width - vis(str)));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function clearLine(content) {
|
|
39
|
-
return `${_c.clearLine}\r${content}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function fmtCoins(n) {
|
|
43
|
-
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
44
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
45
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
46
|
-
return n.toLocaleString();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function fmtUptime(startTime) {
|
|
50
|
-
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
51
|
-
const h = Math.floor(s / 3600);
|
|
52
|
-
const m = Math.floor((s % 3600) / 60);
|
|
53
|
-
const sec = s % 60;
|
|
54
|
-
if (h > 0) return `${h}h ${m}m ${sec}s`;
|
|
55
|
-
if (m > 0) return `${m}m ${sec}s`;
|
|
56
|
-
return `${sec}s`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Render ──────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
function renderDashboard(ctx) {
|
|
62
|
-
if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering) return;
|
|
63
|
-
|
|
64
|
-
init(ctx);
|
|
65
|
-
|
|
66
|
-
const tw = Math.max(process.stdout.columns || 80, 60);
|
|
67
|
-
const _ = _c.reset;
|
|
68
|
-
|
|
69
|
-
// Color shortcuts
|
|
70
|
-
const A = _rgb(139, 92, 246);
|
|
71
|
-
const G = _rgb(52, 211, 153);
|
|
72
|
-
const Au = _rgb(255, 215, 0);
|
|
73
|
-
const O = _rgb(251, 146, 60);
|
|
74
|
-
const Cy = _rgb(34, 211, 238);
|
|
75
|
-
const R = _rgb(239, 68, 68);
|
|
76
|
-
const Y = _rgb(251, 191, 36);
|
|
77
|
-
const W = _c.white;
|
|
78
|
-
const D = _c.dim;
|
|
79
|
-
|
|
80
|
-
// ── Aggregate stats ───────────────────────────────────────────
|
|
81
|
-
let aggBalance = 0, aggCoins = 0, aggCmds = 0, aggErrors = 0;
|
|
82
|
-
for (const w of ctx.workers) {
|
|
83
|
-
aggBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
84
|
-
aggCoins += w.stats.coins || 0;
|
|
85
|
-
aggCmds += w.stats.commands || 0;
|
|
86
|
-
aggErrors += w.stats.errors || 0;
|
|
87
|
-
}
|
|
88
|
-
const successRate = aggCmds > 0 ? Math.round(((aggCmds - aggErrors) / aggCmds) * 100) : 100;
|
|
89
|
-
const elapsedHrs = (Date.now() - ctx.startTime) / 3_600_000;
|
|
90
|
-
const perHr = elapsedHrs > 0.01 ? Math.round(aggCoins / elapsedHrs) : 0;
|
|
91
|
-
const cpmVal = ctx.globalCmdRate.getRate().toFixed(1);
|
|
92
|
-
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
93
|
-
const spin = _spinnerFn('braille');
|
|
94
|
-
|
|
95
|
-
const activeCount = ctx.workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
96
|
-
const invalidCount = ctx.workers.filter(w => w._tokenInvalid).length;
|
|
97
|
-
const pausedCount = ctx.workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
98
|
-
const recovCount = ctx.workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
99
|
-
|
|
100
|
-
const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
|
|
101
|
-
|
|
102
|
-
// ── Column layout ─────────────────────────────────────────────
|
|
103
|
-
const colNum = 4;
|
|
104
|
-
const colSts = 3;
|
|
105
|
-
const colName = Math.max(16, Math.min(22, Math.floor(tw * 0.22)));
|
|
106
|
-
const colBal = 10;
|
|
107
|
-
const colLvl = 5;
|
|
108
|
-
const colLs = 4;
|
|
109
|
-
const colEarn = 9;
|
|
110
|
-
const colAct = Math.max(8, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 12);
|
|
111
|
-
const gap = ' ';
|
|
112
|
-
|
|
113
|
-
// ── Helpers to build rows ─────────────────────────────────────
|
|
114
|
-
const border = (char) => `${A}${char}${D}${'─'.repeat(tw - 2)}${A}${char}${_}`;
|
|
115
|
-
|
|
116
|
-
function mkRow(content, leftPad = ' ') {
|
|
117
|
-
const raw = content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
118
|
-
const padLen = Math.max(0, tw - 4 - raw.length);
|
|
119
|
-
return `${A}│${_}${leftPad}${content}${' '.repeat(padLen)}${leftPad}${A}│${_}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── Build all rows ────────────────────────────────────────────
|
|
123
|
-
const rows = [];
|
|
124
|
-
|
|
125
|
-
// HEADER
|
|
126
|
-
const title = _c.bold + _gradientFn(' DANK GRINDER ', [139, 92, 246], [52, 211, 153]) + _;
|
|
127
|
-
const h1 = [
|
|
128
|
-
title,
|
|
129
|
-
`${D}v${ctx.PKG_VERSION}${_}`,
|
|
130
|
-
`${G}${spin}${_}`,
|
|
131
|
-
`${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_}`,
|
|
132
|
-
`${D}bal${_} ${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_}`,
|
|
133
|
-
`${G}+⏣${fmtCoins(perHr)}${_}/h`,
|
|
134
|
-
`${G}${activeCount}${_}/${W}${ctx.workers.length}${_} active`,
|
|
135
|
-
invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
|
|
136
|
-
pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
|
|
137
|
-
recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
|
|
138
|
-
` ${D}${aggCmds}${_}cmds`,
|
|
139
|
-
` ${D}${cpmVal}${_}/min`,
|
|
140
|
-
` ${D}${memMB}MB`,
|
|
141
|
-
modeTag,
|
|
142
|
-
].filter(Boolean).join(' ');
|
|
143
|
-
|
|
144
|
-
rows.push(border('╔'));
|
|
145
|
-
rows.push(mkRow(h1));
|
|
146
|
-
rows.push(border('╠'));
|
|
147
|
-
rows.push(mkRow(`${D}session${_} ${G}${fmtCoins(aggCoins)}${_} coins ${G}${successRate}%${_} success ${W}${fmtCoins(ctx.sessionPeakCoins)}${_} peak${ctx.isNewHigh ? ` ${Au}★ NEW HIGH${_}` : ''}`));
|
|
148
|
-
rows.push(border('╚'));
|
|
149
|
-
|
|
150
|
-
// ACCOUNTS TABLE
|
|
151
|
-
const headers = [
|
|
152
|
-
`${D}${pad('#', colNum)}${_}`,
|
|
153
|
-
`${D}${pad('S', colSts)}${_}`,
|
|
154
|
-
`${_gradientFn(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
|
|
155
|
-
`${D}${pad('Balance', colBal)}${_}`,
|
|
156
|
-
`${D}${pad('Lvl', colLvl)}${_}`,
|
|
157
|
-
`${D}${pad('LS', colLs)}${_}`,
|
|
158
|
-
`${D}${pad('Earned', colEarn)}${_}`,
|
|
159
|
-
`${D}${pad('Activity', colAct)}${_}`,
|
|
160
|
-
].join(gap);
|
|
161
|
-
|
|
162
|
-
rows.push(border('╔'));
|
|
163
|
-
rows.push(mkRow(headers));
|
|
164
|
-
rows.push(border('╟'));
|
|
165
|
-
|
|
166
|
-
const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
|
|
167
|
-
const maxRows = Math.max(5, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 12)));
|
|
168
|
-
const visible = sorted.slice(0, maxRows);
|
|
169
|
-
const extraRows = sorted.length - maxRows;
|
|
170
|
-
|
|
171
|
-
for (const wk of visible) {
|
|
172
|
-
const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
|
|
173
|
-
|
|
174
|
-
let stsIcon;
|
|
175
|
-
if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
|
|
176
|
-
else if (!wk.running) stsIcon = `${D}○${_}`;
|
|
177
|
-
else if (isRecov) stsIcon = `${O}${_spinnerFn('braille').substring(0, 1)}${_}`;
|
|
178
|
-
else if (wk.paused) stsIcon = `${R}⏸${_}`;
|
|
179
|
-
else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
|
|
180
|
-
else if (wk.busy) stsIcon = `${G}${_spinnerFn('pulse').substring(0, 1)}${_}`;
|
|
181
|
-
else stsIcon = `${G}●${_}`;
|
|
182
|
-
|
|
183
|
-
const name = (wk.username || wk.account.label || '?').substring(0, colName);
|
|
184
|
-
const nameStr = `${wk.color}${name}${_}`;
|
|
185
|
-
const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
|
|
186
|
-
const lvl = wk._level || 0;
|
|
187
|
-
const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
|
|
188
|
-
const ls = wk._lifesavers;
|
|
189
|
-
let lsStr;
|
|
190
|
-
if (ls === 0) lsStr = `${R}♥${ls}${_}`;
|
|
191
|
-
else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
|
|
192
|
-
else if (ls != null) lsStr = `${G}♥${ls}${_}`;
|
|
193
|
-
else {
|
|
194
|
-
const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
|
|
195
|
-
lsStr = `${D}${p}♥?${_}`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const earn = wk.stats.coins || 0;
|
|
199
|
-
const earnStr = earn > 0 ? `${G}+${fmtCoins(earn)}${_}` : `${D}────${_}`;
|
|
200
|
-
const actRaw = (wk.lastStatus || 'ready').replace(/\x1b\[[0-9;]*m/g, '').substring(0, colAct);
|
|
201
|
-
const actStr = `${D}${pad(actRaw, colAct)}${_}`;
|
|
202
|
-
const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
|
|
203
|
-
|
|
204
|
-
const rowStr = [
|
|
205
|
-
numStr,
|
|
206
|
-
pad(stsIcon, colSts),
|
|
207
|
-
pad(nameStr, colName),
|
|
208
|
-
pad(balStr, colBal),
|
|
209
|
-
pad(lvlStr, colLvl),
|
|
210
|
-
pad(lsStr, colLs),
|
|
211
|
-
pad(earnStr, colEarn),
|
|
212
|
-
actStr,
|
|
213
|
-
].join(gap);
|
|
214
|
-
|
|
215
|
-
rows.push(mkRow(rowStr));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (extraRows > 0) {
|
|
219
|
-
rows.push(mkRow(`${D}+${extraRows} more${_}`));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
rows.push(border('╚'));
|
|
223
|
-
|
|
224
|
-
// LIVE FEED
|
|
225
|
-
const logs = ctx.recentLogs.toArray();
|
|
226
|
-
if (logs.length > 0) {
|
|
227
|
-
const feedTitle = `${_gradientFn(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${_spinnerFn('pulse')}${_}`;
|
|
228
|
-
rows.push(border('╔'));
|
|
229
|
-
rows.push(mkRow(feedTitle));
|
|
230
|
-
rows.push(border('╟'));
|
|
231
|
-
|
|
232
|
-
for (const entry of logs) {
|
|
233
|
-
let lineText;
|
|
234
|
-
if (typeof entry === 'string') {
|
|
235
|
-
lineText = entry;
|
|
236
|
-
} else if (entry && typeof entry === 'object') {
|
|
237
|
-
const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
238
|
-
const user = entry.username ? String(entry.username) : '';
|
|
239
|
-
const cmd = entry.command ? `[${entry.command}]` : '';
|
|
240
|
-
const resp = entry.response || '';
|
|
241
|
-
lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
|
|
242
|
-
} else {
|
|
243
|
-
lineText = String(entry);
|
|
244
|
-
}
|
|
245
|
-
rows.push(mkRow(`${D}${lineText}${_}`));
|
|
246
|
-
}
|
|
247
|
-
rows.push(border('╚'));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// FOOTER
|
|
251
|
-
rows.push(border('╔'));
|
|
252
|
-
rows.push(mkRow(`${modeTag} ${D}P=pause R=resume S=status Q=quit${_}`));
|
|
253
|
-
rows.push(border('╚'));
|
|
254
|
-
|
|
255
|
-
// ── Flush: overwrite rows from line 1, then blank remaining screen ──
|
|
256
|
-
const rowsOnScreen = process.stdout.rows || 24;
|
|
257
|
-
for (const row of rows) {
|
|
258
|
-
process.stdout.write(`${_c.clearLine}\r${row}\n`);
|
|
259
|
-
}
|
|
260
|
-
// Fill remaining screen lines with blanks so old startup logs don't bleed through
|
|
261
|
-
const extra = rowsOnScreen - rows.length;
|
|
262
|
-
for (let i = 0; i < extra; i++) {
|
|
263
|
-
process.stdout.write(`${_c.clearLine}\r${' '.repeat(tw)}\n`);
|
|
264
|
-
}
|
|
265
|
-
// Move cursor to end so terminal doesn't scroll on next \r write
|
|
266
|
-
process.stdout.write(`\x1b[${rows.length};1H`);
|
|
267
|
-
|
|
268
|
-
return rows.length;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
module.exports = { renderDashboard };
|
|
6
|
+
module.exports = {};
|
package/lib/grinder.js
CHANGED
|
@@ -3,7 +3,6 @@ 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 { renderDashboard: renderDashboardImpl } = require('./dashboard');
|
|
7
6
|
const {
|
|
8
7
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
9
8
|
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
|
|
@@ -95,10 +94,20 @@ const c = {
|
|
|
95
94
|
};
|
|
96
95
|
|
|
97
96
|
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
|
|
|
99
|
+
|
|
100
|
+
// Simple uptime formatter
|
|
101
|
+
function formatUptime() {
|
|
102
|
+
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
103
|
+
if (s < 60) return `${s}s`;
|
|
104
|
+
const m = Math.floor(s / 60);
|
|
105
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
106
|
+
const h = Math.floor(m / 60);
|
|
107
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
108
|
+
const d = Math.floor(h / 24);
|
|
109
|
+
return `${d}d ${h % 24}h`;
|
|
110
|
+
}
|
|
102
111
|
// ── Safe options for search/crime ──────────────────────────
|
|
103
112
|
// Object.freeze → V8 marks these as immutable, enabling inline caching
|
|
104
113
|
// and preventing accidental mutation across 10K worker instances.
|
|
@@ -133,10 +142,25 @@ const CLUSTER_PREFIX = 'dkg:cluster:';
|
|
|
133
142
|
function initRedis() {
|
|
134
143
|
if (!redis && REDIS_URL) {
|
|
135
144
|
try {
|
|
136
|
-
redis = new Redis(REDIS_URL, {
|
|
137
|
-
|
|
145
|
+
redis = new Redis(REDIS_URL, {
|
|
146
|
+
maxRetriesPerRequest: 3,
|
|
147
|
+
retryStrategy: (times) => times > 5 ? null : Math.min(times * 500, 3000),
|
|
148
|
+
lazyConnect: true,
|
|
149
|
+
});
|
|
150
|
+
redis.connect().catch((e) => {
|
|
151
|
+
// Only warn once — don't spam on persistent connection failures
|
|
152
|
+
if (!redis || redis.status === 'wait') {
|
|
153
|
+
console.warn(`[Redis] connection failed: ${e.message} — continuing without Redis`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
redis.on('error', (e) => {
|
|
157
|
+
// Suppress common transient errors from spamming stderr
|
|
158
|
+
const msg = e?.message || '';
|
|
159
|
+
if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('connect')) return;
|
|
160
|
+
console.error(`[Redis] error: ${msg}`);
|
|
161
|
+
});
|
|
138
162
|
} catch (e) {
|
|
139
|
-
|
|
163
|
+
// Redis optional — continue without it
|
|
140
164
|
}
|
|
141
165
|
}
|
|
142
166
|
}
|
|
@@ -258,17 +282,6 @@ function progressBar(value, max, width, filledColor, emptyColor) {
|
|
|
258
282
|
return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
|
|
259
283
|
}
|
|
260
284
|
|
|
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
|
-
|
|
272
285
|
// ── Box drawing helpers ──────────────────────────────────────
|
|
273
286
|
const BOX = {
|
|
274
287
|
tl: '╭', tr: '╮', bl: '╰', br: '╯',
|
|
@@ -321,123 +334,17 @@ function colorBanner() {
|
|
|
321
334
|
return out;
|
|
322
335
|
}
|
|
323
336
|
|
|
324
|
-
// ──
|
|
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);
|
|
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
|
-
}
|
|
415
|
-
|
|
337
|
+
// ── Simple Logging ─────────────────────────────────────────────
|
|
416
338
|
function log(type, msg, label) {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
cmd:
|
|
339
|
+
const colorIcons = {
|
|
340
|
+
info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
|
|
341
|
+
error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
|
|
342
|
+
cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
|
|
343
|
+
buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
|
|
344
|
+
debug: `${c.dim}·${c.reset}`,
|
|
421
345
|
};
|
|
422
|
-
const
|
|
423
|
-
|
|
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
|
-
}
|
|
346
|
+
const tagCol = label ? `${label} ` : '';
|
|
347
|
+
console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
|
|
441
348
|
}
|
|
442
349
|
|
|
443
350
|
async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
|
|
@@ -869,7 +776,6 @@ class AccountWorker {
|
|
|
869
776
|
|
|
870
777
|
setStatus(text) {
|
|
871
778
|
this.lastStatus = stripAnsi(String(text || '')).replace(/\s+/g, ' ').trim();
|
|
872
|
-
if (dashboardStarted) scheduleRender();
|
|
873
779
|
}
|
|
874
780
|
|
|
875
781
|
waitForDankMemer(timeout = 15000) {
|
|
@@ -1482,34 +1388,32 @@ class AccountWorker {
|
|
|
1482
1388
|
|
|
1483
1389
|
// Update Redis with findings
|
|
1484
1390
|
if (redis) {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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}`);
|
|
1391
|
+
try {
|
|
1392
|
+
if (currentLevel > 0) {
|
|
1393
|
+
await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
|
|
1394
|
+
this._level = currentLevel;
|
|
1498
1395
|
}
|
|
1499
|
-
|
|
1396
|
+
if (lastLifesaverCount >= 0) {
|
|
1397
|
+
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
|
|
1398
|
+
this._lifesavers = lastLifesaverCount;
|
|
1399
|
+
if (lastLifesaverCount === 0) {
|
|
1400
|
+
await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
|
|
1401
|
+
await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
} catch { /* Redis errors non-fatal */ }
|
|
1500
1405
|
}
|
|
1501
1406
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
}
|
|
1407
|
+
return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
|
|
1408
|
+
} catch (e) {
|
|
1409
|
+
lastError = e;
|
|
1410
|
+
if (attempt < maxRetries - 1) {
|
|
1411
|
+
await new Promise(r => setTimeout(r, delays[attempt]));
|
|
1508
1412
|
}
|
|
1509
1413
|
}
|
|
1510
|
-
if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
1511
|
-
return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
|
|
1512
1414
|
}
|
|
1415
|
+
return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
|
|
1416
|
+
}
|
|
1513
1417
|
|
|
1514
1418
|
// ── Run Single Command ──────────────────────────────────────
|
|
1515
1419
|
// Each modular command handler sends the command, waits for response,
|
|
@@ -2844,10 +2748,6 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2844
2748
|
REDIS_URL = process.env.REDIS_URL || '';
|
|
2845
2749
|
WEBHOOK_URL = process.env.WEBHOOK_URL || '';
|
|
2846
2750
|
|
|
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
|
-
|
|
2851
2751
|
// Detect zlib-sync availability
|
|
2852
2752
|
let hasZlib = false;
|
|
2853
2753
|
try { require('zlib-sync'); hasZlib = true; } catch {}
|
|
@@ -2860,8 +2760,6 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2860
2760
|
` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
|
|
2861
2761
|
` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
|
|
2862
2762
|
);
|
|
2863
|
-
console.log(bar);
|
|
2864
|
-
|
|
2865
2763
|
log('info', `${c.dim}Fetching accounts...${c.reset}`);
|
|
2866
2764
|
|
|
2867
2765
|
const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
|
|
@@ -2926,7 +2824,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2926
2824
|
|
|
2927
2825
|
// Init rawLogger Redis (uses same URL — logs all raw gateway data)
|
|
2928
2826
|
if (REDIS_URL) {
|
|
2929
|
-
rawLogger.init(
|
|
2827
|
+
rawLogger.init(redis);
|
|
2930
2828
|
// Listen for DM events across all accounts — update worker state + dashboard LIVE
|
|
2931
2829
|
rawLogger.onDmEvent((event, raw) => {
|
|
2932
2830
|
const channelId = raw.channel_id;
|
|
@@ -2955,13 +2853,11 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2955
2853
|
}
|
|
2956
2854
|
}
|
|
2957
2855
|
}
|
|
2958
|
-
scheduleRender();
|
|
2959
2856
|
}
|
|
2960
2857
|
|
|
2961
2858
|
if (event.type === 'levelup') {
|
|
2962
2859
|
if (event.to > 0) {
|
|
2963
2860
|
w._level = event.to;
|
|
2964
|
-
scheduleRender();
|
|
2965
2861
|
}
|
|
2966
2862
|
}
|
|
2967
2863
|
}
|
|
@@ -2977,110 +2873,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2977
2873
|
console.log(` ${checks.join(' ')}`);
|
|
2978
2874
|
console.log('');
|
|
2979
2875
|
|
|
2980
|
-
// ── Phase 1: Login
|
|
2981
|
-
|
|
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
|
-
}));
|
|
2995
|
-
|
|
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
|
-
};
|
|
3083
|
-
|
|
2876
|
+
// ── Phase 1: Login accounts ─────────────────────────────────────────
|
|
2877
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
|
|
3084
2878
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
3085
2879
|
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
|
|
3086
2880
|
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
|
|
@@ -3096,203 +2890,58 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3096
2890
|
const worker = new AccountWorker(acc, i + idx);
|
|
3097
2891
|
workers.push(worker);
|
|
3098
2892
|
workerMap.set(acc.id, worker);
|
|
3099
|
-
loginStates[i + idx].worker = worker;
|
|
3100
2893
|
await worker.start();
|
|
3101
|
-
finalizeLoginLine(i + idx, worker);
|
|
3102
2894
|
}));
|
|
3103
2895
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
3104
2896
|
hintGC();
|
|
3105
2897
|
}
|
|
3106
2898
|
|
|
3107
|
-
clearInterval(loginSpinnerInterval);
|
|
3108
2899
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
3109
2900
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
3110
2901
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
3111
|
-
console.log(
|
|
3112
|
-
console.log('');
|
|
2902
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${loginDone}/${accounts.length} accounts connected`);
|
|
3113
2903
|
if (invalidWorkers.length > 0) {
|
|
3114
|
-
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens
|
|
3115
|
-
for (const w of invalidWorkers) log('error', `
|
|
3116
|
-
console.log('');
|
|
2904
|
+
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
|
|
2905
|
+
for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
|
|
3117
2906
|
}
|
|
3118
|
-
if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login
|
|
2907
|
+
if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login`);
|
|
3119
2908
|
|
|
3120
2909
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
3121
2910
|
|
|
3122
|
-
// ── Phase 2: Inventory check
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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++;
|
|
2911
|
+
// ── Phase 2: Inventory check ─────────────────────────────────────
|
|
2912
|
+
console.log(` ${rgb(34, 211, 238)}·${c.reset} Checking inventory (${activeWorkers.length} accounts)...`);
|
|
2913
|
+
let invDone = 0, invFailed = 0;
|
|
2914
|
+
await Promise.all(activeWorkers.map(async (w) => {
|
|
2915
|
+
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2916
|
+
catch { invFailed++; return; }
|
|
2917
|
+
invDone++;
|
|
3192
2918
|
}));
|
|
3193
2919
|
|
|
3194
|
-
clearInterval(invSpinnerInterval);
|
|
3195
|
-
process.stdout.write(`\r\x1b[2K`);
|
|
3196
|
-
|
|
3197
2920
|
if (invFailed > 0) {
|
|
3198
|
-
console.log(` ${rgb(239, 68, 68)}✗${c.reset}
|
|
3199
|
-
log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
|
|
2921
|
+
console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed — not starting grind loops`);
|
|
3200
2922
|
return;
|
|
3201
2923
|
}
|
|
3202
|
-
console.log(` ${rgb(52, 211, 153)}✓${c.reset}
|
|
3203
|
-
console.log('');
|
|
2924
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Inventory: ${invDone}/${activeWorkers.length} clear`);
|
|
3204
2925
|
|
|
3205
|
-
// ── Phase 2.5: Balance check
|
|
3206
|
-
|
|
3207
|
-
|
|
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);
|
|
3258
|
-
|
|
3259
|
-
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
2926
|
+
// ── Phase 2.5: Balance check ────────────────────────────────────
|
|
2927
|
+
console.log(` ${rgb(251, 191, 36)}·${c.reset} Checking balance (${activeWorkers.length} accounts)...`);
|
|
2928
|
+
await Promise.all(activeWorkers.map(async (w) => {
|
|
3260
2929
|
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`);
|
|
3274
|
-
balDone++;
|
|
3275
2930
|
}));
|
|
3276
2931
|
|
|
3277
|
-
clearInterval(balSpinnerInterval);
|
|
3278
|
-
process.stdout.write(`\r\x1b[2K`);
|
|
3279
|
-
|
|
3280
2932
|
let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
|
|
3281
2933
|
for (const w of activeWorkers) {
|
|
3282
2934
|
totalWallet += w.stats?.balance || 0;
|
|
3283
2935
|
totalBank += w.stats?.bankBalance || 0;
|
|
3284
2936
|
if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
|
|
3285
2937
|
}
|
|
3286
|
-
console.log(` ${rgb(52, 211, 153)}✓${c.reset}
|
|
2938
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Balance: ${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
|
|
3287
2939
|
if (noLifesaverAccounts.length > 0) {
|
|
3288
|
-
console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${
|
|
2940
|
+
console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
|
|
3289
2941
|
}
|
|
3290
|
-
console.log('');
|
|
3291
|
-
|
|
3292
2942
|
|
|
3293
|
-
// Phase 2.75:
|
|
3294
|
-
|
|
3295
|
-
console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
|
|
2943
|
+
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2944
|
+
console.log(` ${rgb(139, 92, 246)}·${c.reset} Checking DM history...`);
|
|
3296
2945
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
3297
2946
|
for (const w of activeWorkers) {
|
|
3298
2947
|
try {
|
|
@@ -3301,28 +2950,12 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3301
2950
|
if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
|
|
3302
2951
|
if (dm.lifesavers === 0) dmNoLs.push(w.username);
|
|
3303
2952
|
if (dm.lifesavers === -1) dmUnknown.push(w.username);
|
|
3304
|
-
// Store level and lifesaver for dashboard
|
|
3305
2953
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
3306
2954
|
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
|
-
}
|
|
3321
2955
|
} catch {}
|
|
3322
2956
|
}
|
|
3323
2957
|
if (dmNoLs.length > 0) {
|
|
3324
|
-
|
|
3325
|
-
// Set Redis keys to block crime/search
|
|
2958
|
+
log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
3326
2959
|
for (const w of activeWorkers) {
|
|
3327
2960
|
if (dmNoLs.includes(w.username) && redis) {
|
|
3328
2961
|
try {
|
|
@@ -3333,45 +2966,23 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3333
2966
|
}
|
|
3334
2967
|
}
|
|
3335
2968
|
if (dmUnknown.length > 0) {
|
|
3336
|
-
|
|
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.
|
|
2969
|
+
log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
|
|
3339
2970
|
}
|
|
3340
2971
|
const dmSummaryParts = [];
|
|
3341
2972
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3342
2973
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3343
2974
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3344
|
-
|
|
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
|
|
2975
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
|
|
3350
2976
|
|
|
2977
|
+
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2978
|
+
console.log(` ${rgb(139, 92, 246)}>>>${c.reset} Starting grind loops...`);
|
|
3351
2979
|
// Phase 3: Start all grind loops (only for valid workers)
|
|
3352
2980
|
for (const w of activeWorkers) {
|
|
3353
2981
|
if (!shutdownCalled) w.grindLoop();
|
|
3354
2982
|
}
|
|
3355
2983
|
|
|
3356
|
-
|
|
3357
|
-
|
|
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();
|
|
2984
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
|
|
2985
|
+
console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
|
|
3375
2986
|
|
|
3376
2987
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
3377
2988
|
if (CLUSTER_ENABLED) {
|
|
@@ -3428,7 +3039,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3428
3039
|
const before = workers.length;
|
|
3429
3040
|
// Keep ALL workers visible — never remove from array (user wants to see gaps)
|
|
3430
3041
|
// Only clean up workerMap entries for accounts fully removed from API
|
|
3431
|
-
if (workers.length !== before)
|
|
3042
|
+
if (workers.length !== before) { /* workers changed */ }
|
|
3432
3043
|
} catch {}
|
|
3433
3044
|
}, 10_000);
|
|
3434
3045
|
|
|
@@ -3437,18 +3048,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3437
3048
|
if (sigintHandled) return;
|
|
3438
3049
|
sigintHandled = true;
|
|
3439
3050
|
shutdownCalled = true;
|
|
3440
|
-
dashboardStarted = false;
|
|
3441
3051
|
setDashboardActive(false);
|
|
3442
3052
|
process.stdout.write(c.show);
|
|
3443
3053
|
|
|
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
3054
|
const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
|
|
3453
3055
|
console.log('');
|
|
3454
3056
|
console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
|
|
@@ -3510,79 +3112,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3510
3112
|
}
|
|
3511
3113
|
|
|
3512
3114
|
// ══════════════════════════════════════════════════════════════
|
|
3513
|
-
//
|
|
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
|
-
}
|
|
3115
|
+
// Keyboard shortcuts removed — no display to update
|
|
3586
3116
|
|
|
3587
3117
|
// Export the start function for CLI
|
|
3588
3118
|
module.exports = { start };
|
package/lib/rawLogger.js
CHANGED
|
@@ -30,7 +30,14 @@ const memRing = [];
|
|
|
30
30
|
let memIdx = 0;
|
|
31
31
|
|
|
32
32
|
// ── Redis init ──
|
|
33
|
-
async function init(
|
|
33
|
+
async function init(redisUrlOrInstance) {
|
|
34
|
+
// Support passing an existing Redis instance directly (preferred)
|
|
35
|
+
if (redisUrlOrInstance && typeof redisUrlOrInstance.status !== 'undefined') {
|
|
36
|
+
redis = redisUrlOrInstance;
|
|
37
|
+
redisReady = redis.status === 'ready';
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const redisUrl = redisUrlOrInstance;
|
|
34
41
|
if (!redisUrl) {
|
|
35
42
|
console.log('[rawLogger] No Redis URL — raw logging disabled');
|
|
36
43
|
return;
|
|
@@ -54,7 +61,10 @@ async function init(redisUrl) {
|
|
|
54
61
|
redisReady = true;
|
|
55
62
|
console.log('[rawLogger] Redis connected');
|
|
56
63
|
redis.on('error', (e) => {
|
|
57
|
-
|
|
64
|
+
// Suppress common transient network errors from spamming stderr
|
|
65
|
+
const msg = e?.message || '';
|
|
66
|
+
if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('read') || msg.includes('connect')) return;
|
|
67
|
+
console.error(`[rawLogger] Redis error: ${msg}`);
|
|
58
68
|
redisReady = false;
|
|
59
69
|
});
|
|
60
70
|
redis.on('close', () => {
|