dankgrinder 8.40.0 → 8.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/grinder.js CHANGED
@@ -13,12 +13,21 @@ const ui = require('./ui');
13
13
  // Global shutdown flag
14
14
  let shutdownCalled = false;
15
15
 
16
- // Catch silent Discord client errors
16
+ // Catch silent Discord client errors — route through log() once available
17
17
  process.on('unhandledRejection', (reason) => {
18
- process.stdout.write(`[UNHANDLED REJECTION] ${reason?.message || reason}\n`);
18
+ const msg = reason?.message || reason;
19
+ if (typeof log === 'function') {
20
+ log('error', msg);
21
+ } else {
22
+ process.stderr.write(`[UNHANDLED REJECTION] ${msg}\n`);
23
+ }
19
24
  });
20
25
  process.on('uncaughtException', (err) => {
21
- process.stdout.write(`[UNCAUGHT] ${err.message}\n`);
26
+ if (typeof log === 'function') {
27
+ log('error', err.message);
28
+ } else {
29
+ process.stderr.write(`[UNCAUGHT] ${err.message}\n`);
30
+ }
22
31
  });
23
32
 
24
33
  // ── Memory-Optimized Client Factory ──────────────────────────
@@ -2768,7 +2777,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2768
2777
 
2769
2778
  // Init rawLogger Redis (uses same URL — logs all raw gateway data)
2770
2779
  if (REDIS_URL) {
2771
- rawLogger.init(REDIS_URL).catch(() => {});
2780
+ rawLogger.init(REDIS_URL, { silent: true }).catch(() => {});
2772
2781
  // Listen for DM events across all accounts — update worker state + dashboard LIVE
2773
2782
  rawLogger.onDmEvent((event, raw) => {
2774
2783
  const channelId = raw.channel_id;
@@ -2863,7 +2872,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2863
2872
  if (timedOutWorkers.length > 0) {
2864
2873
  ui.log(-1, `${timedOutWorkers.length} timed out (retrying in background)`);
2865
2874
  }
2866
- ui.draw();
2867
2875
 
2868
2876
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2869
2877
 
@@ -2919,10 +2927,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2919
2927
 
2920
2928
  // ── Phase 3: Start grind loops ───────────────────────────────────
2921
2929
  ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
2930
+ ui.setLive(true);
2931
+ ui.draw();
2922
2932
  for (const w of activeWorkers) {
2923
2933
  if (!shutdownCalled) w.grindLoop();
2924
2934
  }
2925
- ui.draw();
2926
2935
 
2927
2936
  // Cluster heartbeat — lets other nodes see this node is alive
2928
2937
  if (CLUSTER_ENABLED) {
package/lib/rawLogger.js CHANGED
@@ -28,11 +28,15 @@ const memStore = new Map();
28
28
  const channelLast = new Map();
29
29
  const memRing = [];
30
30
  let memIdx = 0;
31
+ let _silent = false;
32
+ function _log(...args) { if (!_silent) console.log(...args); }
33
+ function _err(...args) { if (!_silent) console.error(...args); }
31
34
 
32
35
  // ── Redis init ──
33
- async function init(redisUrl) {
36
+ async function init(redisUrl, opts = {}) {
37
+ if (opts.silent) _silent = true;
34
38
  if (!redisUrl) {
35
- console.log('[rawLogger] No Redis URL — raw logging disabled');
39
+ _log('[rawLogger] No Redis URL — raw logging disabled');
36
40
  return;
37
41
  }
38
42
  try {
@@ -44,7 +48,7 @@ async function init(redisUrl) {
44
48
  });
45
49
  // Skip if already connecting or connected
46
50
  if (redis.status === 'connecting' || redis.status === 'connect' || redis.status === 'ready') {
47
- console.log('[rawLogger] Redis already connecting — skipping');
51
+ _log('[rawLogger] Redis already connecting — skipping');
48
52
  redisReady = false;
49
53
  return;
50
54
  }
@@ -58,12 +62,12 @@ async function init(redisUrl) {
58
62
  redis.connect().catch(reject);
59
63
  });
60
64
  redisReady = true;
61
- console.log('[rawLogger] Redis connected');
65
+ _log('[rawLogger] Redis connected');
62
66
  redis.on('error', (e) => {
63
67
  // Suppress common transient network errors from spamming stderr
64
68
  const msg = e?.message || '';
65
69
  if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('read') || msg.includes('connect')) return;
66
- console.error(`[rawLogger] Redis error: ${msg}`);
70
+ _err(`[rawLogger] Redis error: ${msg}`);
67
71
  redisReady = false;
68
72
  });
69
73
  redis.on('close', () => {
@@ -79,7 +83,7 @@ async function init(redisUrl) {
79
83
  // Suppress "already connecting" errors — happens when Redis reconnects mid-init
80
84
  const msg = e?.message || '';
81
85
  if (!msg.includes('already connecting') && !msg.includes('already connected')) {
82
- console.error(`[rawLogger] Redis connect failed: ${msg}`);
86
+ _err(`[rawLogger] Redis connect failed: ${msg}`);
83
87
  }
84
88
  redis = null;
85
89
  redisReady = false;
package/lib/ui.js CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
- * CLI Live Dashboard — box design, loading animations, per-account status.
3
- * Everything inside a single box. Events stream below. No duplicate output.
2
+ * CLI Live Dashboard — fixed box at top, events stream below.
3
+ * Box drawn once. Events appended line-by-line. No flicker.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
7
7
  let _workers = [];
8
8
  let _isShuttingDown = () => false;
9
9
  let _version = '0.0.0';
10
+ let _live = false;
11
+ let _boxHeight = 0; // rows consumed by the box
10
12
 
11
13
  // ── Spinner frames ────────────────────────────────────────────
12
14
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -60,11 +62,6 @@ const DIM = c.dim;
60
62
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
61
63
  function padR(s, n) { return trunc(s, n).padEnd(n); }
62
64
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
63
- function padC(s, n) {
64
- s = String(s || '');
65
- const pad = Math.max(0, n - s.length);
66
- return ' '.repeat(Math.floor(pad / 2)) + s + ' '.repeat(Math.ceil(pad / 2));
67
- }
68
65
 
69
66
  function fmtUptime() {
70
67
  const s = Math.floor((Date.now() - _startTime) / 1000);
@@ -104,25 +101,25 @@ function statusText(w) {
104
101
  function layout() {
105
102
  const W = Math.min(process.stdout.columns || 120, 120);
106
103
  const rows = process.stdout.rows || 40;
107
- const bannerH = BANNER_LINES.length + 1; // +1 for blank line
104
+ const bannerH = BANNER_LINES.length + 1; // +1 blank
108
105
  const statusH = 1;
109
- const headerH = 3; // blank + headers + hr
110
- const totalsH = 2; // totals + hr
111
- const footerH = 1; // blank
112
- const maxAccounts = Math.min(_workers.length, Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - 8));
113
- const eventH = Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - maxAccounts);
114
- return { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH };
106
+ const headerH = 2; // headers + hr
107
+ const footerH = 2; // totals + hr
108
+ const maxAccounts = Math.min(_workers.length, Math.max(3, rows - bannerH - statusH - headerH - footerH - 5));
109
+ return { W, bannerH, statusH, headerH, footerH, maxAccounts };
115
110
  }
116
111
 
117
- // ── Draw the full box ────────────────────────────────────────
112
+ // ── Draw the fixed box ────────────────────────────────────────
118
113
  function draw() {
119
- const { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH } = layout();
120
- const T = '─'.repeat(W - 2); // inner width
114
+ const { W, bannerH, statusH, headerH, footerH, maxAccounts } = layout();
115
+ const T = '─'.repeat(W - 2);
116
+
117
+ // ── Save cursor, clear screen, go to top ──
118
+ process.stdout.write('\x1b[s'); // save cursor
119
+ process.stdout.write('\x1b[2J\x1b[H'); // clear screen + home
120
+ process.stdout.write('\x1b[1G'); // column 1
121
121
 
122
122
  // ── Top of box ──
123
- // Move cursor to top-left of box area and overwrite in place (no full clear)
124
- process.stdout.write(`\x1b[${1};1H`);
125
- process.stdout.write(`\x1b[0J`); // clear from cursor to end of screen
126
123
  process.stdout.write(`\x1b[38;2;77;212;238m┌─${T}─┐\x1b[0m\n`);
127
124
 
128
125
  // ── Banner inside box ──
@@ -144,18 +141,13 @@ function draw() {
144
141
  errors > 0 ? `${c.red}E${c.reset}${errors}` : null,
145
142
  `${DIM}Ctrl+C${c.reset}`,
146
143
  ].filter(Boolean).join(' ');
147
- const statusPad = W - 4 - statusLine.length;
144
+ const statusPad = W - 4 - stripAnsi(statusLine).length;
148
145
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusLine}${' '.repeat(Math.max(0, statusPad))} \x1b[38;2;77;212;238m│\x1b[0m\n`);
149
146
  process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
150
147
 
151
148
  // ── Table header ──
152
149
  const col = {
153
- st: 8, // Status
154
- name: 18, // Account name
155
- last: 20, // Last command
156
- cmds: 6, // Commands
157
- ok: 4, // OK%
158
- earned: 10, // Earned
150
+ st: 9, name: 20, last: 22, cmds: 6, ok: 5, earned: 10,
159
151
  };
160
152
  const nameExtra = Math.max(0, W - 4 - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 14);
161
153
  col.name += nameExtra;
@@ -164,7 +156,7 @@ function draw() {
164
156
  process.stdout.write(`${c.bold}#${c.reset} `);
165
157
  process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
166
158
  process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
167
- process.stdout.write(`${c.bold}${padR('LAST COMMAND', col.last)}${c.reset} `);
159
+ process.stdout.write(`${c.bold}${padR('LAST CMD', col.last)}${c.reset} `);
168
160
  process.stdout.write(`${c.bold}${padL('CMDS', col.cmds)}${c.reset} `);
169
161
  process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
170
162
  process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
@@ -186,14 +178,14 @@ function draw() {
186
178
  const wi = _workers.indexOf(w);
187
179
  const col2 = wc(wi);
188
180
  const stCol = statusColor(w);
189
- const stText = statusText(w);
181
+ const stTxt = statusText(w);
190
182
  const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
191
183
  const cmds = w.stats.commands || 0;
192
184
  const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
193
185
 
194
186
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
195
187
  process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
196
- process.stdout.write(`${stCol}${padR(stText, col.st)}${c.reset} `);
188
+ process.stdout.write(`${stCol}${padR(stTxt, col.st)}${c.reset} `);
197
189
  process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
198
190
  process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
199
191
  process.stdout.write(`${padL(cmds, col.cmds)} `);
@@ -203,7 +195,7 @@ function draw() {
203
195
  }
204
196
 
205
197
  if (sorted.length > maxAccounts) {
206
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+${sorted.length - maxAccounts} more accounts${' '.repeat(Math.max(0, W - 24 - String(sorted.length - maxAccounts).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
198
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+${sorted.length - maxAccounts} more${' '.repeat(Math.max(0, W - 20 - String(sorted.length - maxAccounts).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
207
199
  }
208
200
 
209
201
  // ── Totals ──
@@ -221,50 +213,31 @@ function draw() {
221
213
  process.stdout.write(`${c.bold}Σ${c.reset} `);
222
214
  process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
223
215
  process.stdout.write(`${' '.repeat(col.name)} `);
224
- process.stdout.write(`${DIM}${' '.repeat(col.last)}${c.reset} `);
216
+ process.stdout.write(`${' '.repeat(col.last)} `);
225
217
  process.stdout.write(`${padL(totalCmds, col.cmds)} `);
226
218
  process.stdout.write(`${padL(rate, col.ok)}% `);
227
219
  process.stdout.write(`${totalCoins > 0 ? c.green + padL('+' + totalCoins.toLocaleString(), col.earned) + c.reset : DIM + padL('—', col.earned) + c.reset} `);
228
220
  process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(W - 4));
229
221
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
230
222
 
231
- // ── Events section ──
232
- process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
233
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(Math.max(0, W - 10))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
234
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
223
+ // ── Bottom of box ──
224
+ process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
235
225
 
236
- // Show recent events per account
237
- const evLines = [];
238
- for (let i = 0; i < _workers.length; i++) {
239
- if (_eventLines[i] && _eventLines[i].length > 0) {
240
- const latest = _eventLines[i][_eventLines[i].length - 1];
241
- const col2 = wc(i);
242
- const name = trunc(_workers[i]?.username || '?', 14);
243
- evLines.push({ i, text: latest.text, ts: latest.ts, col: col2, name });
244
- }
245
- }
226
+ // Record how many rows the box consumed
227
+ _boxHeight = bannerH + statusH + 1 + headerH + shown.length + (sorted.length > maxAccounts ? 1 : 0) + footerH + 1;
246
228
 
247
- for (let i = 0; i < Math.min(eventH, evLines.length); i++) {
248
- const ev = evLines[evLines.length - 1 - i];
249
- const now = new Date();
250
- const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
251
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
252
- process.stdout.write(`${ev.col}${padR(ev.name, 14)}${c.reset} `);
253
- process.stdout.write(`${DIM}[${ts}]${c.reset} ${ev.text}${' '.repeat(Math.max(0, W - 20 - ev.name.length - ev.text.length))}`);
254
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
255
- }
256
-
257
- for (let i = 0; i < Math.max(0, eventH - evLines.length); i++) {
258
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${' '.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
259
- }
229
+ // Restore cursor to just below the box
230
+ process.stdout.write(`\x1b[u`); // restore cursor
231
+ }
260
232
 
261
- // ── Bottom of box ──
262
- process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
233
+ // ── Strip ANSI for length calc ───────────────────────────────
234
+ function stripAnsi(s) {
235
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '');
263
236
  }
264
237
 
265
238
  // ── Event tracking ────────────────────────────────────────────
266
239
  let _eventLines = []; // [accountIdx] = [{text, ts}]
267
- const MAX_EVENTS = 3;
240
+ const MAX_EVENTS = 2;
268
241
 
269
242
  // ── Public API ────────────────────────────────────────────────
270
243
 
@@ -274,28 +247,41 @@ function init({ workers, isShuttingDown }) {
274
247
  _isShuttingDown = isShuttingDown || (() => false);
275
248
  _version = '0.0.0';
276
249
  _eventLines = [];
250
+ _live = false;
277
251
  }
278
252
 
279
253
  function drawBanner(version) { _version = version; }
280
254
  function start() {}
281
- function stop() { process.stdout.write(c.reset + '\n'); }
255
+ function stop() { _live = false; process.stdout.write(c.reset + '\n'); }
256
+
257
+ // Enable live mode — from this point, log() appends below the box
258
+ function setLive(val) { _live = val; }
282
259
 
283
260
  function log(accountIdx, msg) {
284
261
  const now = new Date();
285
262
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
286
263
 
264
+ // Track event per account
287
265
  if (accountIdx >= 0) {
288
266
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
289
267
  _eventLines[accountIdx].push({ text: msg, ts });
290
268
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
291
269
  }
292
270
 
293
- draw();
271
+ if (!_live) return; // during login: just track, don't output yet
272
+
273
+ // ── Append event LINE BY LINE below the box ──
274
+ const col2 = accountIdx >= 0 ? wc(accountIdx) : '';
275
+ const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 12) : 'GLOBAL';
276
+
277
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
278
+ process.stdout.write(`${col2}${padR(name, 12)}${c.reset} `);
279
+ process.stdout.write(`${DIM}[${ts}]${c.reset} ${msg}`);
280
+ process.stdout.write(`\n`);
294
281
  }
295
282
 
296
283
  function logGlobal(msg) {
297
- // Global events go to account 0
298
284
  log(-1, msg);
299
285
  }
300
286
 
301
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop };
287
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.40.0",
3
+ "version": "8.42.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"