dankgrinder 6.45.0 → 6.46.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.
Files changed (2) hide show
  1. package/lib/grinder.js +74 -161
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -3217,39 +3217,21 @@ async function start(apiKey, apiUrl) {
3217
3217
  console.log(` ${checks.join(' ')}`);
3218
3218
  console.log('');
3219
3219
 
3220
- // ── Phase 1: Login with per-account inline rendering ─────────────────────────
3220
+ // ── Phase 1: Login — inline table with per-row updates ─────────
3221
3221
  const startupTw = process.stdout.columns || 90;
3222
- const colNum = 4; // " #"
3223
- const colSts = 3; // "ST"
3222
+ const colNum = 4;
3223
+ const colSts = 3;
3224
3224
  const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
3225
3225
  const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
3226
3226
  const colCmds = 8;
3227
3227
  const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
3228
3228
 
3229
- const loginStates = accounts.map((acc, i) => ({
3230
- name: acc.label || acc.id || '?',
3231
- done: false,
3232
- failed: false,
3233
- worker: null,
3234
- }));
3235
-
3236
- let loginLines = [];
3237
- loginLines.push(` ${'─'.repeat(loginVis)}`);
3238
- for (let i = 0; i < loginStates.length; i++) {
3239
- const s = loginStates[i];
3240
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3241
- const name = s.name.substring(0, colName).padEnd(colName);
3242
- const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
3243
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3244
- loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
3245
- }
3246
- loginLines.push(` ${'─'.repeat(loginVis)}`);
3247
- for (const l of loginLines) console.log(l);
3248
-
3249
- // Dynamically capture the starting row of the login table via DSR
3250
- let loginBaseRow = 1;
3251
- const captureLoginRow = () => new Promise(resolve => {
3252
- process.stdout.write(MARKER);
3229
+ // Use DSR to find starting row, then use explicit row numbers for all table writes.
3230
+ // This avoids relying on cursor tracking via \n which varies by terminal.
3231
+ let tableTopRow = 1;
3232
+ let pendingSet = new Set(Array.from({ length: accounts.length }, (_, i) => i));
3233
+ const captureTopRow = () => new Promise(resolve => {
3234
+ process.stdout.write('\x1b[6n');
3253
3235
  const chunks = [];
3254
3236
  const handler = (chunk) => {
3255
3237
  chunks.push(chunk);
@@ -3257,51 +3239,52 @@ async function start(apiKey, apiUrl) {
3257
3239
  const m = raw.match(/\x1b\[(\d+);\d+R/);
3258
3240
  if (m) {
3259
3241
  process.stdin.removeListener('data', handler);
3260
- loginBaseRow = parseInt(m[1], 10) + 1;
3242
+ tableTopRow = parseInt(m[1], 10);
3261
3243
  resolve();
3262
3244
  }
3263
3245
  };
3264
3246
  process.stdin.on('data', handler);
3265
3247
  setTimeout(resolve, 50);
3266
3248
  });
3267
- await captureLoginRow();
3268
-
3269
- let loginPending = new Array(accounts.length).fill(true);
3270
- const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3249
+ await captureTopRow();
3250
+
3251
+ // Absolute row numbers for all table elements (calculated from captured top row)
3252
+ const borderTopRow = tableTopRow; // border
3253
+ const dataStartRow = tableTopRow + 1; // first account row
3254
+ const borderBotRow = tableTopRow + accounts.length + 1; // bottom border
3255
+ const bottomRow = borderBotRow + 1; // cursor final position after table
3256
+
3257
+ // Print initial table using explicit row positioning
3258
+ process.stdout.write(`\x1b[${borderTopRow};1H ${'─'.repeat(loginVis)}`);
3259
+ for (let i = 0; i < accounts.length; i++) {
3260
+ const row = dataStartRow + i;
3261
+ const name = (accounts[i].label || accounts[i].id || '?').substring(0, colName).padEnd(colName);
3262
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3263
+ process.stdout.write(`\x1b[${row};1H ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'···'.padEnd(colGuild)}${c.reset} ${c.dim}${'···'.padEnd(colCmds)}${c.reset}\x1b[K`);
3264
+ }
3265
+ process.stdout.write(`\x1b[${borderBotRow};1H ${'─'.repeat(loginVis)}\x1b[K`);
3271
3266
 
3267
+ // Spinner: updates rows inline using absolute row numbers
3272
3268
  const drawLoginSpinners = () => {
3273
- for (let i = 0; i < loginPending.length; i++) {
3274
- if (!loginPending[i]) continue;
3269
+ for (const i of pendingSet) {
3275
3270
  const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3271
+ const name = (accounts[i].label || accounts[i].id || '?').substring(0, colName).padEnd(colName);
3276
3272
  const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3277
- const name = loginStates[i].name.substring(0, colName).padEnd(colName);
3278
- const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
3279
- const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3280
- const row = loginBaseRow + 1 + i; // +1 skips the top border line
3281
- moveToRow(row);
3282
- process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
3283
- }
3284
- // Move cursor back to bottom to avoid overwriting the bottom border
3285
- const lastRow = loginBaseRow + 1 + accounts.length + 1;
3286
- moveToRow(lastRow);
3273
+ process.stdout.write(`\x1b[${dataStartRow + i};1H ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${c.dim}${'logging in...'.substring(0, colGuild)}${c.reset} ${c.dim}${'···'.padEnd(colCmds)}${c.reset}\x1b[K`);
3274
+ }
3287
3275
  };
3288
- const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3289
-
3290
- const finalizeLoginLine = (idx, worker) => {
3291
- if (!loginPending[idx]) return;
3292
- loginPending[idx] = false;
3293
- const s = loginStates[idx];
3294
- s.done = true;
3295
- s.worker = worker;
3276
+ const loginSpinner = setInterval(drawLoginSpinners, 80);
3296
3277
 
3278
+ const finalizeLoginRow = (idx, worker) => {
3279
+ if (!pendingSet.has(idx)) return;
3280
+ pendingSet.delete(idx);
3297
3281
  const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
3298
- const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3282
+ const name = (worker.username || accounts[idx].label || accounts[idx].id || '?').substring(0, colName).padEnd(colName);
3299
3283
  let sts, guild, cmds;
3300
3284
  if (worker._tokenInvalid) {
3301
3285
  sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3302
3286
  guild = 'INVALID'.padEnd(colGuild);
3303
3287
  cmds = '···'.padEnd(colCmds);
3304
- s.failed = true;
3305
3288
  } else if (worker.channel) {
3306
3289
  sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3307
3290
  const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
@@ -3312,9 +3295,7 @@ async function start(apiKey, apiUrl) {
3312
3295
  guild = 'timeout'.padEnd(colGuild);
3313
3296
  cmds = '···'.padEnd(colCmds);
3314
3297
  }
3315
- const row = loginBaseRow + 1 + idx; // +1 skips the top border line
3316
- moveToRow(row);
3317
- process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3298
+ process.stdout.write(`\x1b[${dataStartRow + idx};1H ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3318
3299
  };
3319
3300
 
3320
3301
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
@@ -3332,58 +3313,34 @@ async function start(apiKey, apiUrl) {
3332
3313
  const worker = new AccountWorker(acc, i + idx);
3333
3314
  workers.push(worker);
3334
3315
  workerMap.set(acc.id, worker);
3335
- loginStates[i + idx].worker = worker;
3336
3316
  await worker.start();
3337
- finalizeLoginLine(i + idx, worker);
3317
+ finalizeLoginRow(i + idx, worker);
3338
3318
  }));
3339
3319
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3340
3320
  hintGC();
3341
3321
  }
3342
3322
 
3343
- clearInterval(loginSpinnerInterval);
3344
- const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3323
+ clearInterval(loginSpinner);
3345
3324
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3346
3325
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3347
- console.log(`\r\x1b[2K ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3348
- console.log('');
3326
+ const activeWorkers = workers.filter(w => !w._tokenInvalid);
3327
+ const loginDone = activeWorkers.filter(w => w.channel).length;
3328
+ // Clear bottom border row, move to new line, print login complete
3329
+ process.stdout.write(`\x1b[${borderBotRow};1H\x1b[2K\n ${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}\n`);
3349
3330
  if (invalidWorkers.length > 0) {
3350
- log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3351
- for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3352
- console.log('');
3331
+ console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}${c.red}${invalidWorkers.length} INVALID token(s):${c.reset} ${invalidWorkers.map(w => w.account.label || w.account.id).join(', ')}`);
3353
3332
  }
3354
3333
  if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3334
+ console.log('');
3355
3335
 
3356
- const activeWorkers = workers.filter(w => !w._tokenInvalid);
3336
+ // ── Phase 2: Inventory check — clean sequential table ─────────
3357
3337
 
3358
- // ── Phase 2: Inventory check — spinner for pending count, results inline ─────────
3359
3338
  const iColNum = 4;
3360
3339
  const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3361
3340
  const iColItems = 8;
3362
3341
  const iColVal = 16;
3363
3342
  const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3364
3343
 
3365
- // Print a unique marker, query its position, then overwrite it with the table
3366
- process.stdout.write(MARKER);
3367
- let invBaseRow = 1;
3368
- const captureRow = () => new Promise(resolve => {
3369
- const chunks = [];
3370
- const handler = (chunk) => {
3371
- chunks.push(chunk);
3372
- const raw = chunks.join('');
3373
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3374
- if (m) {
3375
- process.stdin.removeListener('data', handler);
3376
- invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
3377
- resolve();
3378
- }
3379
- };
3380
- process.stdin.on('data', handler);
3381
- setTimeout(resolve, 50);
3382
- });
3383
- await captureRow();
3384
-
3385
- // Now print the inventory table starting at invBaseRow
3386
- const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3387
3344
  console.log(` ${'─'.repeat(invVis)}`);
3388
3345
  for (let i = 0; i < activeWorkers.length; i++) {
3389
3346
  const w = activeWorkers[i];
@@ -3393,40 +3350,31 @@ async function start(apiKey, apiUrl) {
3393
3350
  }
3394
3351
  console.log(` ${'─'.repeat(invVis)}`);
3395
3352
 
3396
- let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3397
- const drawInvProgress = () => {
3398
- if (invPending === 0) return;
3399
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3400
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3401
- const barW = Math.min(20, startupTw - 40);
3402
- const filled = Math.round(pct * barW);
3403
- const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3404
- const pctStr = `${Math.round(pct * 100)}%`;
3405
- invMoveToRow(invBaseRow);
3406
- 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`);
3407
- };
3408
- const invSpinnerInterval = setInterval(drawInvProgress, 80);
3353
+ const invResults = await Promise.all(activeWorkers.map(async (w, i) => {
3354
+ try {
3355
+ return await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 5, silent: true });
3356
+ } catch {
3357
+ return { ok: false };
3358
+ }
3359
+ }));
3409
3360
 
3410
- await Promise.all(activeWorkers.map(async (w, i) => {
3361
+ let invDone = 0, invFailed = 0;
3362
+ // Re-print table with results
3363
+ console.log(` ${'─'.repeat(invVis)}`);
3364
+ for (let i = 0; i < activeWorkers.length; i++) {
3365
+ const invRes = invResults[i] || { ok: false };
3366
+ const w = activeWorkers[i];
3411
3367
  const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3412
3368
  const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3413
- let invRes;
3414
- try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 5, silent: true }); }
3415
- catch { invRes = { ok: false }; }
3416
- invPending--;
3417
3369
  const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3418
3370
  const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3419
3371
  const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3420
3372
  const itemStr = `${items}`.padEnd(iColItems);
3421
3373
  const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3422
- const row = invBaseRow + 1 + i;
3423
- invMoveToRow(row);
3424
- process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3374
+ console.log(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}`);
3425
3375
  if (invRes?.ok) invDone++; else invFailed++;
3426
- }));
3427
-
3428
- clearInterval(invSpinnerInterval);
3429
- process.stdout.write(`\r\x1b[2K`);
3376
+ }
3377
+ console.log(` ${'─'.repeat(invVis)}`);
3430
3378
 
3431
3379
  if (invFailed > 0) {
3432
3380
  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}`);
@@ -3436,7 +3384,7 @@ async function start(apiKey, apiUrl) {
3436
3384
  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}`);
3437
3385
  console.log('');
3438
3386
 
3439
- // ── Phase 2.5: Balance check — inline table, single spinner for progress ─────────
3387
+ // ── Phase 2.5: Balance check — clean sequential table ─────────
3440
3388
  const bColNum = 4;
3441
3389
  const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3442
3390
  const bColWallet = 12;
@@ -3445,27 +3393,6 @@ async function start(apiKey, apiUrl) {
3445
3393
  const bColLs = 4;
3446
3394
  const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3447
3395
 
3448
- // Capture starting row for balance phase
3449
- process.stdout.write(MARKER);
3450
- let balBaseRow = 1;
3451
- const balCaptureRow = () => new Promise(resolve => {
3452
- const chunks = [];
3453
- const handler = (chunk) => {
3454
- chunks.push(chunk);
3455
- const raw = chunks.join('');
3456
- const m = raw.match(/\x1b\[(\d+);\d+R/);
3457
- if (m) {
3458
- process.stdin.removeListener('data', handler);
3459
- balBaseRow = parseInt(m[1], 10) + 1;
3460
- resolve();
3461
- }
3462
- };
3463
- process.stdin.on('data', handler);
3464
- setTimeout(resolve, 50);
3465
- });
3466
- await balCaptureRow();
3467
-
3468
- const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
3469
3396
  console.log(` ${'─'.repeat(balVis)}`);
3470
3397
  for (let i = 0; i < activeWorkers.length; i++) {
3471
3398
  const w = activeWorkers[i];
@@ -3475,22 +3402,14 @@ async function start(apiKey, apiUrl) {
3475
3402
  }
3476
3403
  console.log(` ${'─'.repeat(balVis)}`);
3477
3404
 
3478
- let balDone = 0, balPending = activeWorkers.length;
3479
- const drawBalProgress = () => {
3480
- if (balPending === 0) return;
3481
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3482
- const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3483
- const barW = Math.min(20, startupTw - 40);
3484
- const filled = Math.round(pct * barW);
3485
- const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3486
- balMoveToRow(balBaseRow);
3487
- 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`);
3488
- };
3489
- const balSpinnerInterval = setInterval(drawBalProgress, 80);
3490
-
3491
- await Promise.all(activeWorkers.map(async (w, i) => {
3405
+ await Promise.all(activeWorkers.map(async (w) => {
3492
3406
  try { await w.checkBalance(true); } catch {}
3493
- balPending--;
3407
+ }));
3408
+
3409
+ // Re-print table with results
3410
+ console.log(` ${'─'.repeat(balVis)}`);
3411
+ for (let i = 0; i < activeWorkers.length; i++) {
3412
+ const w = activeWorkers[i];
3494
3413
  const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3495
3414
  const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3496
3415
  const wallet = w.stats?.balance || 0;
@@ -3500,14 +3419,9 @@ async function start(apiKey, apiUrl) {
3500
3419
  const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3501
3420
  const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3502
3421
  const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3503
- const row = balBaseRow + 1 + i;
3504
- balMoveToRow(row);
3505
- 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`);
3506
- balDone++;
3507
- }));
3508
-
3509
- clearInterval(balSpinnerInterval);
3510
- process.stdout.write(`\r\x1b[2K`);
3422
+ console.log(` ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}`);
3423
+ }
3424
+ console.log(` ${'─'.repeat(balVis)}`);
3511
3425
 
3512
3426
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3513
3427
  for (const w of activeWorkers) {
@@ -3521,7 +3435,6 @@ async function start(apiKey, apiUrl) {
3521
3435
  }
3522
3436
  console.log('');
3523
3437
 
3524
-
3525
3438
  // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3526
3439
  console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3527
3440
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.45.0",
3
+ "version": "6.46.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"