dankgrinder 6.19.0 → 6.21.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.
@@ -16,11 +16,19 @@ const {
16
16
  const { Trie, VoseAlias, LRUCache } = require('../structures');
17
17
 
18
18
  const SAFE_CRIME_OPTIONS = Object.freeze([
19
- 'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
19
+ // 100% safe (from logs analysis)
20
+ 'identity theft', 'fraud', 'littering',
21
+ // 67% safe
22
+ 'dui',
23
+ // Other potentially safe options
24
+ 'tax evasion', 'cybercrime', 'hacking',
20
25
  'money laundering', 'tax fraud', 'insurance fraud', 'scam',
21
26
  ]);
22
27
 
23
28
  const RISKY_CRIME_OPTIONS = Object.freeze([
29
+ // 0% safe (from logs)
30
+ 'cyber bullying', 'trespassing', 'shoplifting',
31
+ // Known dangerous
24
32
  'murder', 'arson', 'assault', 'kidnap', 'terrorism',
25
33
  ]);
26
34
 
@@ -16,9 +16,12 @@ const {
16
16
  const { VoseAlias, Trie, EMA, LRUCache } = require('../structures');
17
17
 
18
18
  const SAFE_SEARCH_LOCATIONS = Object.freeze([
19
+ // 100% safe (from logs analysis)
20
+ 'shoe', 'washer', 'attic', 'pocket',
21
+ // Other safe locations
19
22
  'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
20
- 'couch', 'pantry', 'fridge', 'kitchen', 'bathroom', 'attic',
21
- 'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
23
+ 'couch', 'pantry', 'fridge', 'kitchen', 'bathroom',
24
+ 'closet', 'vacuum', 'toilet', 'sink', 'shower',
22
25
  'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
23
26
  ]);
24
27
 
package/lib/grinder.js CHANGED
@@ -487,7 +487,7 @@ function renderDashboard() {
487
487
  lines.push(bEmpty);
488
488
 
489
489
  // ═══════════════════════════════════════════════════════════════
490
- // STATS PANEL
490
+ // STATS PANEL (split: left = metrics, right = big trend)
491
491
  // ═══════════════════════════════════════════════════════════════
492
492
  lines.push(bSep);
493
493
  lines.push(bEmpty);
@@ -495,32 +495,44 @@ function renderDashboard() {
495
495
  // Earnings sparkline data
496
496
  const now = Date.now();
497
497
  if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
498
- const sparkW = Math.min(30, Math.floor(iw * 0.3));
499
- const spark = drawSparkline(earningsHistory.toArray(), sparkW);
500
-
501
- // Row 1: Balance + Earned
502
498
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
503
499
  const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
504
500
  const peakFlag = isNewHigh ? ` ${R}${c.bold}* NEW HIGH *${c.reset}` : '';
505
501
 
506
- lines.push(bRow(` ${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣ ${formatCoins(totalBalance)}${c.reset} ${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}+⏣ ${formatCoins(totalCoins)}${c.reset} ${D}(${c.reset}${G}${formatCoins(perHr)}/h${c.reset}${D})${c.reset}${peakFlag}`));
507
-
508
- // Row 2: Peak + Trend sparkline
509
- lines.push(bRow(` ${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${A}~${c.reset} ${D}TREND${c.reset} ${spark}`));
510
-
511
- // Row 3: Commands + Success + Rate + Uptime
502
+ // Left column: fixed metrics (left-aligned)
512
503
  const cpmVal = globalCmdRate.getRate().toFixed(1);
513
504
  const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
514
- const srBarW = Math.min(15, Math.floor(iw * 0.12));
505
+ const srBarW = Math.min(15, Math.floor(iw * 0.1));
515
506
  const srBar = progressBar(successRate, 100, srBarW, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
516
- lines.push(bRow(` ${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
517
-
518
- // Row 4: Memory
519
507
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
520
508
  const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
521
- const memBarW = Math.min(20, Math.floor(iw * 0.15));
509
+ const memBarW = Math.min(15, Math.floor(iw * 0.1));
522
510
  const memBar = progressBar(memMB, 1024, memBarW, memCol, [40, 40, 55]);
523
- lines.push(bRow(` ${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`));
511
+
512
+ // Right column: big trend sparkline
513
+ const sparkW = Math.floor(iw * 0.42);
514
+ const spark = drawSparkline(earningsHistory.toArray(), sparkW);
515
+
516
+ // Build split rows
517
+ const leftHalf = Math.floor(iw * 0.52);
518
+
519
+ // Row 1: Balance + EARNED | TREND (big sparkline)
520
+ const leftRow1 = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣ ${formatCoins(totalBalance)}${c.reset} ${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}+⏣ ${formatCoins(totalCoins)}${c.reset}${peakFlag}`;
521
+ const rightRow1 = `${A}~${c.reset} ${D}TREND${c.reset} ${spark}`;
522
+ const combined1 = `${leftRow1}${' '.repeat(Math.max(2, leftHalf - leftRow1.replace(RE, '').length))}${rightRow1}`;
523
+ lines.push(bRow(` ${combined1}`));
524
+
525
+ // Row 2: Peak (left)
526
+ const peakRow = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣ ${formatCoins(sessionPeakCoins)}${c.reset}`;
527
+ lines.push(bRow(` ${peakRow}${' '.repeat(Math.max(2, leftHalf - peakRow.replace(RE, '').length))}${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
528
+
529
+ // Row 3: CMDS (left) + success bar + rate
530
+ const cmdsRow = `${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset}`;
531
+ lines.push(bRow(` ${cmdsRow}`));
532
+
533
+ // Row 4: MEM (left)
534
+ const memRow = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
535
+ lines.push(bRow(` ${memRow}`));
524
536
 
525
537
  lines.push(bEmpty);
526
538
 
@@ -1499,7 +1511,7 @@ class AccountWorker {
1499
1511
  }
1500
1512
  }
1501
1513
 
1502
- async checkBalance() {
1514
+ async checkBalance(silent = false) {
1503
1515
  const prefix = this.account.use_slash ? '/' : 'pls';
1504
1516
  const sentAt = Date.now();
1505
1517
 
@@ -1594,7 +1606,7 @@ class AccountWorker {
1594
1606
 
1595
1607
  this.stats.balance = wallet;
1596
1608
  this.stats.bankBalance = bank;
1597
- this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} ${c.dim}(${matched || 'none'})${c.reset}`);
1609
+ if (!silent) this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset}`);
1598
1610
 
1599
1611
  // Store in Redis for persistence
1600
1612
  if (redis) {
@@ -1619,13 +1631,17 @@ class AccountWorker {
1619
1631
  } catch { /* silent */ }
1620
1632
  }
1621
1633
 
1622
- // ── Check DM History for deaths/level-ups ──────────────────
1634
+ // ── Check DM History for deaths/level-ups (with retry) ─────
1623
1635
  async checkDmHistory() {
1624
- try {
1625
- const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
1626
- const dm = await dankUser.createDM();
1627
- this._dmChannelId = dm.id;
1628
- const recent = await dm.messages.fetch({ limit: 20 });
1636
+ const maxRetries = 3;
1637
+ const delays = [1000, 2000, 4000];
1638
+ let lastError;
1639
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1640
+ try {
1641
+ const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
1642
+ const dm = await dankUser.createDM();
1643
+ this._dmChannelId = dm.id;
1644
+ const recent = await dm.messages.fetch({ limit: 20 });
1629
1645
 
1630
1646
  let deaths = 0, levelUps = 0, currentLevel = 0, lastLifesaverCount = -1;
1631
1647
  for (const [, msg] of recent) {
@@ -1679,11 +1695,16 @@ class AccountWorker {
1679
1695
  }
1680
1696
  }
1681
1697
 
1682
- return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1683
- } catch (e) {
1684
- this.log('debug', `DM check failed: ${e.message}`);
1685
- return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1698
+ return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
1699
+ } catch (e) {
1700
+ lastError = e;
1701
+ if (attempt < maxRetries - 1) {
1702
+ await new Promise(r => setTimeout(r, delays[attempt]));
1703
+ }
1704
+ }
1686
1705
  }
1706
+ this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1707
+ return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1687
1708
  }
1688
1709
 
1689
1710
  // ── Run Single Command ──────────────────────────────────────
@@ -2993,25 +3014,82 @@ async function start(apiKey, apiUrl) {
2993
3014
  console.log(` ${checks.join(' ')}`);
2994
3015
  console.log('');
2995
3016
 
2996
- // ── Animated loading bar helper ──────────────────────────────
2997
- const barW = Math.min(40, (process.stdout.columns || 80) - 30);
2998
- let loginDone = 0;
2999
- const drawLoginProgress = () => {
3000
- const pct = accounts.length > 0 ? loginDone / accounts.length : 0;
3001
- const filled = Math.round(pct * barW);
3002
- const empty = barW - filled;
3003
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3004
- const bar = rgb(139, 92, 246) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
3005
- const pctStr = `${Math.round(pct * 100)}%`;
3006
- process.stdout.write(`\r ${rgb(139, 92, 246)}${spin}${c.reset} ${c.dim}Logging in...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}${pctStr}${c.reset} `);
3017
+ // ── Per-account inline login UI ──────────────────────────────
3018
+ // Track login state per account for inline rendering
3019
+ const loginStates = accounts.map((acc, i) => ({
3020
+ name: acc.label || acc.id || '?',
3021
+ done: false,
3022
+ failed: false,
3023
+ worker: null,
3024
+ workerIdx: i,
3025
+ }));
3026
+
3027
+ // Column widths (visible chars)
3028
+ const terminalW = process.stdout.columns || 90;
3029
+ const colNum = 4; // " # "
3030
+ const colSts = 3; // "ST "
3031
+ const colName = Math.min(24, Math.max(12, Math.floor(terminalW * 0.25)));
3032
+ const colGuild = Math.min(20, Math.max(8, Math.floor(terminalW * 0.2)));
3033
+ const colCmds = 10; // " 20 cmds"
3034
+ const totalVis = colNum + colSts + colName + colGuild + colCmds + 8;
3035
+
3036
+ // Print header + all account lines (initial pending state)
3037
+ console.log(` ${'─'.repeat(totalVis)}`);
3038
+ for (let i = 0; i < loginStates.length; i++) {
3039
+ const s = loginStates[i];
3040
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3041
+ const name = s.name.substring(0, colName).padEnd(colName);
3042
+ console.log(`${num}${c.dim}PN${c.reset} ${name} ${c.dim}${'···'.padEnd(colGuild)}${c.reset} ${c.dim}···${c.reset}`);
3043
+ }
3044
+ console.log(` ${'─'.repeat(totalVis)}`);
3045
+
3046
+ // Draw pending spinners
3047
+ const drawLoginSpinners = () => {
3048
+ for (let i = 0; i < loginStates.length; i++) {
3049
+ const s = loginStates[i];
3050
+ if (s.done || s.failed) continue;
3051
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3052
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3053
+ const name = s.name.substring(0, colName).padEnd(colName);
3054
+ const guild = 'logging in...'.substring(0, colGuild).padEnd(colGuild);
3055
+ process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${c.dim}${guild}${c.reset} ${c.dim}···${c.reset}`);
3056
+ }
3057
+ process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3007
3058
  };
3059
+ const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3060
+
3061
+ // Update a line when an account finishes
3062
+ const finalizeLoginLine = (idx, worker) => {
3063
+ const s = loginStates[idx];
3064
+ if (s.done || s.failed) return;
3065
+ s.done = true;
3066
+ s.worker = worker;
3067
+
3068
+ const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset} `;
3069
+ const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3070
+
3071
+ let sts, guild, cmds;
3072
+ if (worker._tokenInvalid) {
3073
+ sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3074
+ guild = 'INVALID'.padEnd(colGuild);
3075
+ cmds = '···';
3076
+ s.failed = true;
3077
+ } else if (worker.channel) {
3078
+ sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3079
+ const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
3080
+ guild = gn.padEnd(colGuild);
3081
+ cmds = `${worker.stats?.commands || 0}cmds`;
3082
+ } else {
3083
+ sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
3084
+ guild = 'timeout'.padEnd(colGuild);
3085
+ cmds = '···';
3086
+ }
3008
3087
 
3009
- // Progress animation timer
3010
- const progressInterval = setInterval(drawLoginProgress, 80);
3088
+ const line = ` ${num}${sts} ${name} ${guild} ${cmds}`;
3089
+ process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3090
+ };
3011
3091
 
3012
- // Phase 1: Login all accounts (optimized for speed)
3013
- const LOGIN_PROGRESS_EVERY = 10;
3014
- // Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
3092
+ // Phase 1: Login in batches of 10
3015
3093
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
3016
3094
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
3017
3095
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
@@ -3022,16 +3100,12 @@ async function start(apiKey, apiUrl) {
3022
3100
  return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
3023
3101
  };
3024
3102
 
3025
- // Parallel login in batches of 10 to avoid rate limits while being fast
3026
- // Within each batch, stagger logins by 100-600ms to avoid gateway flood
3027
3103
  const BATCH_SIZE = 10;
3028
3104
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
3029
3105
  if (shutdownCalled) break;
3030
3106
  const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
3031
3107
 
3032
- // Staggered parallel login: fire each login with a small jitter delay
3033
3108
  await Promise.all(batch.map(async (acc, idx) => {
3034
- // Stagger within batch: 0ms for first, 100-600ms for subsequent
3035
3109
  if (idx > 0) {
3036
3110
  const jitter = 100 + Math.floor(Math.random() * 500);
3037
3111
  await new Promise(r => setTimeout(r, jitter));
@@ -3039,30 +3113,29 @@ async function start(apiKey, apiUrl) {
3039
3113
  const worker = new AccountWorker(acc, i + idx);
3040
3114
  workers.push(worker);
3041
3115
  workerMap.set(acc.id, worker);
3116
+ loginStates[i + idx].worker = worker;
3042
3117
  await worker.start();
3043
- loginDone++;
3118
+ finalizeLoginLine(i + idx, worker);
3044
3119
  }));
3045
3120
 
3046
- // Small gap between batches
3047
3121
  if (i + BATCH_SIZE < accounts.length) {
3048
- const gapMs = randomLoginGap();
3049
- await new Promise(r => setTimeout(r, gapMs));
3122
+ await new Promise(r => setTimeout(r, randomLoginGap()));
3050
3123
  }
3051
3124
 
3052
3125
  hintGC();
3053
3126
  }
3054
3127
 
3055
- clearInterval(progressInterval);
3056
- // Clear the progress line and show done
3057
- process.stdout.write(`\r${c.clearLine}`);
3058
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}/${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3059
- console.log('');
3128
+ clearInterval(loginSpinnerInterval);
3129
+ process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3060
3130
 
3061
- // Login summary: show invalid tokens clearly
3131
+ // Final summary
3132
+ const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3062
3133
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3063
3134
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3135
+ console.log(` ${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}`);
3136
+ console.log('');
3137
+
3064
3138
  if (invalidWorkers.length > 0) {
3065
- console.log('');
3066
3139
  log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3067
3140
  for (const w of invalidWorkers) {
3068
3141
  log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
@@ -3076,85 +3149,176 @@ async function start(apiKey, apiUrl) {
3076
3149
  // Filter out workers with invalid tokens from grinding
3077
3150
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3078
3151
 
3079
- // Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
3080
- console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking inventory for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${c.dim} accounts...${c.reset}`);
3081
-
3082
- // Animated inventory progress
3083
- let invDone = 0;
3084
- let invFailed = 0;
3085
- const total = activeWorkers.length;
3086
- const invBarW = Math.min(40, (process.stdout.columns || 80) - 30);
3087
-
3088
- const drawInvProgress = () => {
3089
- const pct = total > 0 ? invDone / total : 0;
3090
- const filled = Math.round(pct * invBarW);
3091
- const empty = invBarW - filled;
3092
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3093
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
3094
- process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${total}${c.reset} ${c.dim}${Math.round(pct * 100)}%${c.reset} `);
3152
+ // ── Phase 2: Inventory check with per-account inline rendering ─────────────────
3153
+ const invStates = activeWorkers.map((w, i) => ({
3154
+ name: w.username || w.account.label || '?',
3155
+ idx: i,
3156
+ done: false,
3157
+ failed: false,
3158
+ items: 0,
3159
+ value: 0,
3160
+ attempt: 0,
3161
+ worker: w,
3162
+ }));
3163
+
3164
+ const tw2 = process.stdout.columns || 90;
3165
+ const iColNum = 4;
3166
+ const iColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3167
+ const iColItems = 8;
3168
+ const iColVal = 14;
3169
+ const iColTries = 10;
3170
+ const iTotalVis = iColNum + iColName + iColItems + iColVal + iColTries + 10;
3171
+
3172
+ console.log(` ${'─'.repeat(iTotalVis)}`);
3173
+ for (let i = 0; i < invStates.length; i++) {
3174
+ const s = invStates[i];
3175
+ const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3176
+ const name = s.name.substring(0, iColName).padEnd(iColName);
3177
+ console.log(`${num}${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'··'.padEnd(iColVal)}${c.reset} ${c.dim}${''.padEnd(iColTries)}${c.reset}`);
3178
+ }
3179
+ console.log(` ${'─'.repeat(iTotalVis)}`);
3180
+
3181
+ let invDone = 0, invFailed = 0;
3182
+
3183
+ const drawInvSpinners = () => {
3184
+ for (let i = 0; i < invStates.length; i++) {
3185
+ const s = invStates[i];
3186
+ if (s.done || s.failed) continue;
3187
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3188
+ const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3189
+ const name = s.name.substring(0, iColName).padEnd(iColName);
3190
+ const items = 'checking'.substring(0, iColItems).padEnd(iColItems);
3191
+ const val = ''.padEnd(iColVal);
3192
+ const tries = s.attempt > 0 ? `try ${s.attempt}/3` : '';
3193
+ process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(34, 211, 238)}${spin}${c.reset} ${name} ${c.dim}${items}${c.reset} ${c.dim}${val}${c.reset} ${c.dim}${tries}${c.reset}`);
3194
+ }
3195
+ process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3095
3196
  };
3197
+ const invSpinnerInterval = setInterval(drawInvSpinners, 80);
3198
+
3199
+ const finalizeInvLine = (idx, invRes) => {
3200
+ const s = invStates[idx];
3201
+ if (s.done || s.failed) return;
3202
+ if (invRes?.ok) {
3203
+ s.done = true;
3204
+ invDone++;
3205
+ } else {
3206
+ s.failed = true;
3207
+ invFailed++;
3208
+ }
3096
3209
 
3097
- const invProgressInterval = setInterval(drawInvProgress, 80);
3210
+ const num = `${c.dim}${(idx + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3211
+ const name = s.name.substring(0, iColName).padEnd(iColName);
3212
+ const items = `${invRes?.items?.length || 0}`.padEnd(iColItems);
3213
+ const val = invRes?.ok
3214
+ ? `${c.green}⏣${(invRes.totalValue || 0).toLocaleString()}${c.reset}`.padEnd(iColVal + 3)
3215
+ : `${c.dim}··${c.reset}`.padEnd(iColVal);
3216
+ const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3217
+
3218
+ const line = ` ${num}${sts} ${name} ${items} ${val}`;
3219
+ process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3220
+ };
3098
3221
 
3099
3222
  await Promise.all(activeWorkers.map(async (w, i) => {
3100
3223
  try {
3101
- const invRes = await w.checkInventory({
3102
- force: true,
3103
- startupProgress: { current: i + 1, total },
3104
- requireComplete: true,
3105
- maxAttempts: 3,
3106
- });
3107
- if (invRes?.ok) invDone++;
3108
- else invFailed++;
3224
+ const invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3 });
3225
+ finalizeInvLine(i, invRes);
3109
3226
  } catch {
3110
- invFailed++;
3227
+ finalizeInvLine(i, { ok: false });
3111
3228
  }
3112
3229
  }));
3113
3230
 
3114
- clearInterval(invProgressInterval);
3115
- process.stdout.write(`\r${c.clearLine}`);
3231
+ clearInterval(invSpinnerInterval);
3232
+ process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3116
3233
 
3117
- // Final summary
3118
3234
  if (invFailed > 0) {
3119
- 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}${total}${c.reset} done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
3235
+ 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}`);
3120
3236
  log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
3121
3237
  return;
3122
3238
  }
3123
3239
 
3124
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${total}${c.reset} ${c.dim}all clear${c.reset}`);
3240
+ 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}`);
3125
3241
  console.log('');
3126
3242
 
3127
- // Phase 2.5: Check balance for ALL accounts sequentially (CV2 needs raw logger timing)
3128
- console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking balance for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${c.dim} accounts...${c.reset}`);
3243
+ // ── Phase 2.5: Balance check with inline per-account rendering ─────────────────
3244
+ const balStates = activeWorkers.map((w, i) => ({
3245
+ name: w.username || w.account.label || '?',
3246
+ idx: i,
3247
+ done: false,
3248
+ wallet: 0,
3249
+ bank: 0,
3250
+ worker: w,
3251
+ }));
3252
+
3253
+ const bColNum = 4;
3254
+ const bColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3255
+ const bColWallet = 14;
3256
+ const bColBank = 14;
3257
+ const bColTotal = 14;
3258
+ const bColLs = 4;
3259
+ const bTotalVis = bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 12;
3260
+
3261
+ console.log(` ${'─'.repeat(bTotalVis)}`);
3262
+ for (let i = 0; i < balStates.length; i++) {
3263
+ const s = balStates[i];
3264
+ const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3265
+ const name = s.name.substring(0, bColName).padEnd(bColName);
3266
+ 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}`);
3267
+ }
3268
+ console.log(` ${'─'.repeat(bTotalVis)}`);
3129
3269
 
3130
3270
  let balDone = 0;
3131
- const balProgressInterval = setInterval(() => {
3132
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3133
- process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${c.bold}${rgb(52, 211, 153)}${balDone}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} `);
3134
- }, 80);
3135
3271
 
3136
- // Run in parallel
3272
+ const drawBalSpinners = () => {
3273
+ for (let i = 0; i < balStates.length; i++) {
3274
+ const s = balStates[i];
3275
+ if (s.done) continue;
3276
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3277
+ const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3278
+ const name = s.name.substring(0, bColName).padEnd(bColName);
3279
+ process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(251, 191, 36)}${spin}${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}`);
3280
+ }
3281
+ process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3282
+ };
3283
+ const balSpinnerInterval = setInterval(drawBalSpinners, 80);
3284
+
3285
+ const finalizeBalLine = (idx, w) => {
3286
+ const s = balStates[idx];
3287
+ if (s.done) return;
3288
+ s.done = true;
3289
+ s.wallet = w.stats?.balance || 0;
3290
+ s.bank = w.stats?.bankBalance || 0;
3291
+ balDone++;
3292
+
3293
+ const num = `${c.dim}${(idx + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3294
+ const name = (w.username || s.name || '?').substring(0, bColName).padEnd(bColName);
3295
+ const ls = w._lifesavers ?? '?';
3296
+ const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3297
+ const wallet = `${c.green}⏣${(s.wallet).toLocaleString()}${c.reset}`.padEnd(bColWallet + 3);
3298
+ const bank = `${c.cyan}⏣${(s.bank).toLocaleString()}${c.reset}`.padEnd(bColBank + 3);
3299
+ const total = `${c.bold}⏣${(s.wallet + s.bank).toLocaleString()}${c.reset}`.padEnd(bColTotal + 3);
3300
+
3301
+ const line = ` ${num}${rgb(52, 211, 153)}✓${c.reset} ${name} ${wallet} ${bank} ${total} ${lsColor}♥${ls}${c.reset}`;
3302
+ process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3303
+ };
3304
+
3137
3305
  await Promise.all(activeWorkers.map(async w => {
3138
3306
  try {
3139
- await w.checkBalance();
3140
- balDone++;
3307
+ await w.checkBalance(true); // silent: don't spam console during inline rendering
3141
3308
  } catch {}
3309
+ const idx = balStates.findIndex(s => s.worker === w);
3310
+ if (idx >= 0) finalizeBalLine(idx, w);
3142
3311
  }));
3143
3312
 
3144
- clearInterval(balProgressInterval);
3145
- process.stdout.write(`\r${c.clearLine}`);
3313
+ clearInterval(balSpinnerInterval);
3314
+ process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3146
3315
 
3147
- // Show balance + lifesaver summary for each account
3316
+ // Balance summary
3148
3317
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3149
- for (const w of activeWorkers) {
3150
- const wallet = w.stats?.balance || 0;
3151
- const bank = w.stats?.bankBalance || 0;
3152
- const ls = w._lifesavers ?? '?';
3153
- totalWallet += wallet;
3154
- totalBank += bank;
3155
- const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3156
- console.log(` ${c.dim}├${c.reset} ${c.bold}${w.username}${c.reset} Wallet: ${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} LS: ${lsColor}${ls}${c.reset}`);
3157
- if (ls === 0) noLifesaverAccounts.push(w.username);
3318
+ for (const s of balStates) {
3319
+ totalWallet += s.wallet;
3320
+ totalBank += s.bank;
3321
+ if (s.worker._lifesavers === 0) noLifesaverAccounts.push(s.name);
3158
3322
  }
3159
3323
  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}`);
3160
3324
 
package/lib/rawLogger.js CHANGED
@@ -152,18 +152,30 @@ function detectCommand(d) {
152
152
  if (cv2Text.includes('fishing') || cv2Text.includes('fisherfolk')) return 'fish';
153
153
  if (cv2Text.includes('deposit') || cv2Text.includes('bank account')) return 'deposit';
154
154
  if (cv2Text.includes('begging') || cv2Text.includes('imagine begging')) return 'beg';
155
- if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle')) return 'hunt';
156
- if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig')) return 'dig';
155
+ if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle') || cv2Text.includes('your aim was so bad') || cv2Text.includes('animals laughed') || cv2Text.includes('animals attacked') || cv2Text.includes('barely escaped') || cv2Text.includes('fell asleep in a tree') || cv2Text.includes('caught nothing') || cv2Text.includes('brought back literally nothing') || cv2Text.includes('rifle broke')) return 'hunt';
156
+ if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig') || cv2Text.includes('dug in the dirt') || cv2Text.includes('brought back') && (cv2Text.includes('ant') || cv2Text.includes('worm') || cv2Text.includes('stickbug') || cv2Text.includes('ladybug'))) return 'dig';
157
157
  if (cv2Text.includes('great work') || cv2Text.includes('for your shift') || cv2Text.includes('working as') || cv2Text.includes('work shift') || cv2Text.includes('what color was') || cv2Text.includes('remember words order') || cv2Text.includes('remember the colors') || cv2Text.includes('remember the emojis') || cv2Text.includes('what word was repeated') || cv2Text.includes('unscramble') || cv2Text.includes('remember the number') || cv2Text.includes('click the buttons in correct order') || cv2Text.includes('babysitter') || cv2Text.includes('click the matching')) return 'work';
158
158
  if (cv2Text.includes('weekly')) return 'weekly';
159
159
  if (cv2Text.includes('daily')) return 'daily';
160
160
  if (cv2Text.includes('inventory')) return 'inventory';
161
161
  if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
162
+ if (cv2Text.includes('balances') && cv2Text.includes('global rank')) return 'balance';
163
+
164
+ // Check content text (plain message content)
165
+ const contentText = (d.content || '').toLowerCase();
166
+ if (contentText.includes('balances') && contentText.includes('global rank')) return 'balance';
167
+ if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed')) return 'hunt';
168
+ if (contentText.includes('imagine going into the woods')) return 'hunt';
169
+ if (contentText.includes('you ran an ad for') && contentText.includes('received')) return 'stream';
170
+ if (contentText.includes('you can\'t interact with your stream')) return 'stream';
171
+ if (contentText.includes('you dug in the dirt') || contentText.includes('found nothing while digging')) return 'dig';
162
172
 
163
173
  // Check embed text
164
174
  const embedText = extractEmbedText(d.embeds).toLowerCase();
165
175
  // Gambling
166
176
  if (embedText.includes('high') && embedText.includes('low') && embedText.includes('secret number')) return 'highlow';
177
+ if (embedText.includes('you won') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
178
+ if (embedText.includes('you lost') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
167
179
  if (embedText.includes('blackjack') || embedText.includes('dealer')) return 'blackjack';
168
180
  if (embedText.includes('roulette')) return 'roulette';
169
181
  if (embedText.includes('spinning') && embedText.includes('slots')) return 'slots';
@@ -175,10 +187,18 @@ function detectCommand(d) {
175
187
  if (embedText.includes('what crime do you want')) return 'crime';
176
188
  if (embedText.includes('where do you want to search')) return 'search';
177
189
  if (embedText.includes('you searched') || embedText.includes('searched the')) return 'search';
190
+ if (embedText.includes('committed') && (embedText.includes('trespassing') || embedText.includes('identity theft') || embedText.includes('fraud') || embedText.includes('shoplifting') || embedText.includes('dui') || embedText.includes('tax evasion') || embedText.includes('littering') || embedText.includes('cyber bullying') || embedText.includes('grand theft auto') || embedText.includes('drug distribution') || embedText.includes('bank robbing') || embedText.includes('arson') || embedText.includes('murder') || embedText.includes('vandalism') || embedText.includes('jaywalking') || embedText.includes('piracy') || embedText.includes('breaking and entering'))) return 'crime';
178
191
  if (embedText.includes('you committed') || embedText.includes('went outside')) return 'crime';
192
+ if (embedText.includes('stole a developer') || embedText.includes('got confused about what trespassing')) return 'crime';
193
+ // Search results (person names) - also check for beg results
194
+ if ((embedText.includes('oh you poor soul') || embedText.includes('take this') || embedText.includes('sure take') || embedText.includes('here\'s a thought') || embedText.includes('nope, nothing') || embedText.includes('no u') || embedText.includes('coins? in this economy')) && (embedText.includes('###') || embedText.includes('charlie chaplin') || embedText.includes('shrek') || embedText.includes('elton john') || embedText.includes('alexa') || embedText.includes('confucius') || embedText.includes('doctor strange') || embedText.includes('rick astley') || embedText.includes('toby turner') || embedText.includes('oprah') || embedText.includes('bruce lee') || embedText.includes('david attenborough') || embedText.includes('honey badger'))) {
195
+ // Check if it's a beg result (has life saver or specific beg text)
196
+ if (embedText.includes('life saver') || embedText.includes('lifesaver')) return 'beg';
197
+ return 'search';
198
+ }
179
199
  // Hunt / dig
180
- if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('hunt') && (embedText.includes('caught') || embedText.includes('brought back') || embedText.includes('attacked') || embedText.includes('nothing') || embedText.includes('laughed') || embedText.includes('escaped') || embedText.includes('fell asleep'))) return 'hunt';
181
- if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('found a') && embedText.includes('dig') || embedText.includes('shovel') || embedText.includes('found nothing while') || embedText.includes('you found') && !embedText.includes('search')) return 'dig';
200
+ if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('your aim was so bad') || embedText.includes('animals laughed') || embedText.includes('animals attacked') || embedText.includes('barely escaped') || embedText.includes('fell asleep in a tree') || embedText.includes('caught nothing') || embedText.includes('brought back literally nothing') || embedText.includes('rifle broke') || embedText.includes('imagine going into the woods')) return 'hunt';
201
+ if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('dug in the dirt') || embedText.includes('found nothing while') || embedText.includes('what are the odds lol') || embedText.includes('brought back') && (embedText.includes('ant') || embedText.includes('worm') || embedText.includes('stickbug') || embedText.includes('ladybug'))) return 'dig';
182
202
  // Work — match both minigame prompt AND completion
183
203
  if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color') || embedText.includes('babysitter') || embedText.includes('great work') || embedText.includes('for your shift'))) return 'work';
184
204
  if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
@@ -187,11 +207,20 @@ function detectCommand(d) {
187
207
  // Postmemes
188
208
  if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
189
209
  // Stream
190
- if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream')) return 'stream';
210
+ if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream') || embedText.includes('you ran an ad for') || embedText.includes('you received') && embedText.includes('from your sponsors') || embedText.includes('### chat') && embedText.includes('hasanbabi')) return 'stream';
211
+ if (embedText.includes('you can\'t interact with your stream') || embedText.includes('stream can last')) return 'stream';
191
212
  // Deposit
192
213
  if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
214
+ // Balance
215
+ if (embedText.includes('balances') && embedText.includes('global rank') && embedText.includes('net worth')) return 'balance';
193
216
  // Trivia
194
- if (embedText.includes('you have 10 seconds to answer') || embedText.includes('trivia')) return 'trivia';
217
+ if (embedText.includes('you have 10 seconds to answer') || embedText.includes('you have 12 seconds to answer') || embedText.includes('you have 15 seconds to answer') || embedText.includes('trivia') || embedText.includes('difficulty') && embedText.includes('category') && (embedText.includes('correct answer was') || embedText.includes('you got that answer correct'))) return 'trivia';
218
+ if (embedText.includes('who in pulp fiction') || embedText.includes('what was') || embedText.includes('which of')) return 'trivia';
219
+ // Cooldown messages
220
+ if (embedText.includes('you can work again at') || embedText.includes('you can use this command again')) return 'cooldown';
221
+ if (embedText.includes('amount needs to be greater than 0')) return 'cooldown';
222
+ // Premium/upgrade messages
223
+ if (embedText.includes('you can buy the ability to use this command')) return 'premium';
195
224
  // Profile / level
196
225
  if (embedText.includes('level:') && embedText.includes('experience:')) return 'profile';
197
226
  // Shop
@@ -218,7 +247,15 @@ function parseRawPacket(d, event) {
218
247
  const embedText = extractEmbedText(d.embeds);
219
248
  const isCV2 = !!(d.flags & 32768);
220
249
  const isEphemeral = !!(d.flags & 64);
221
- const command = detectCommand(d);
250
+
251
+ // For UPDATE events, try to preserve the original command classification
252
+ let command = detectCommand(d);
253
+ if (event === 'UPDATE' && command === 'unknown') {
254
+ const existing = memStore.get(d.id);
255
+ if (existing && existing.command && existing.command !== 'unknown') {
256
+ command = existing.command;
257
+ }
258
+ }
222
259
 
223
260
  return {
224
261
  id: d.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.19.0",
3
+ "version": "6.21.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"