dankgrinder 7.7.0 → 7.12.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/bin/dankgrinder.js +2 -2
- package/lib/commands/farm.js +266 -114
- package/lib/grinder.js +12 -4
- package/package.json +1 -1
package/bin/dankgrinder.js
CHANGED
|
@@ -33,7 +33,7 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
33
33
|
process.exit(0);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
let apiKey = process.env.DANKGRINDER_KEY || '';
|
|
36
|
+
let apiKey = process.env.DANKGRINDER_KEY || process.env.GRINDER_API_KEY || '';
|
|
37
37
|
let apiUrl = '';
|
|
38
38
|
let redisUrl = '';
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
43
43
|
if (args[i] === '--redis' && args[i + 1]) redisUrl = args[i + 1];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
apiUrl = apiUrl || process.env.DANKGRINDER_URL || DEFAULT_URL;
|
|
46
|
+
apiUrl = apiUrl || process.env.DANKGRINDER_URL || process.env.GRINDER_URL || DEFAULT_URL;
|
|
47
47
|
if (redisUrl) process.env.REDIS_URL = redisUrl;
|
|
48
48
|
|
|
49
49
|
// Keep process alive on transient discord interaction fetch failures.
|
package/lib/commands/farm.js
CHANGED
|
@@ -5,15 +5,9 @@ const {
|
|
|
5
5
|
} = require('./utils');
|
|
6
6
|
const { buyItem, buyItemsBatch } = require('./shop');
|
|
7
7
|
const rawLogger = require('../../lib/rawLogger');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
analyzeFarmGrid,
|
|
12
|
-
gridToString,
|
|
13
|
-
evaluateActionNeed,
|
|
14
|
-
evaluateActionScores,
|
|
15
|
-
dumpFarmVisionDebug,
|
|
16
|
-
} = require('./farmVision');
|
|
8
|
+
// NOTE: Vision/ML image analysis is disabled — farm decisions are purely based on
|
|
9
|
+
// ephemeral responses and text detection. Re-enable by importing:
|
|
10
|
+
// const { downloadImage, extractFarmImageUrl, analyzeFarmGrid, gridToString } = require('./farmVision');
|
|
17
11
|
|
|
18
12
|
const RE_TS = /<t:(\d+):R>/;
|
|
19
13
|
const RE_MIN = /(\d+)\s*minute/i;
|
|
@@ -414,6 +408,7 @@ function logEphemeralLike(tag, payload) {
|
|
|
414
408
|
else LOG.info(line);
|
|
415
409
|
}
|
|
416
410
|
|
|
411
|
+
/* DISABLED — vision-based image analysis for action scoring
|
|
417
412
|
async function analyzeFarmImageForAction(msg, actionName) {
|
|
418
413
|
try {
|
|
419
414
|
const url = extractFarmImageUrl(msg);
|
|
@@ -486,6 +481,9 @@ function getVisionRepairPlan(actionName, score) {
|
|
|
486
481
|
return null;
|
|
487
482
|
}
|
|
488
483
|
|
|
484
|
+
/* DISABLED — vision-based action scoring (no longer used)
|
|
485
|
+
// ── Vision-based next-action inference ─────────────────────────────────────────
|
|
486
|
+
// Rely purely on ephemeral responses instead of ML image analysis.
|
|
489
487
|
function inferNextActionFromScore(actionName, score) {
|
|
490
488
|
if (!score) return null;
|
|
491
489
|
const planted = score.ratios?.planted || 0;
|
|
@@ -505,7 +503,9 @@ function inferNextActionFromScore(actionName, score) {
|
|
|
505
503
|
}
|
|
506
504
|
return null;
|
|
507
505
|
}
|
|
506
|
+
*/
|
|
508
507
|
|
|
508
|
+
/* DISABLED — vision-based preferred action inference
|
|
509
509
|
async function inferPreferredActionFromImage(msg) {
|
|
510
510
|
try {
|
|
511
511
|
const url = extractFarmImageUrl(msg);
|
|
@@ -518,43 +518,18 @@ async function inferPreferredActionFromImage(msg) {
|
|
|
518
518
|
planted: (analysis?.counts?.planted || 0) / slots,
|
|
519
519
|
unknown: (analysis?.counts?.unknown || 0) / slots,
|
|
520
520
|
};
|
|
521
|
-
|
|
522
521
|
let suggested = null;
|
|
523
|
-
// Image-first phase routing (3-state: tilled/planted/unknown, no separate wet state).
|
|
524
522
|
if (ratios.planted >= 0.30) suggested = 'plant';
|
|
525
523
|
else if (ratios.tilled >= 0.30) suggested = 'water';
|
|
526
524
|
else suggested = 'hoe';
|
|
527
|
-
|
|
528
|
-
let dbg = null;
|
|
529
|
-
try {
|
|
530
|
-
dbg = await dumpFarmVisionDebug({
|
|
531
|
-
imgBuffer: buf,
|
|
532
|
-
analysis,
|
|
533
|
-
actionName: `phase-${suggested || 'unknown'}`,
|
|
534
|
-
sourceUrl: url,
|
|
535
|
-
});
|
|
536
|
-
} catch (e) {
|
|
537
|
-
LOG.warn(`[farm:phase-image] debug dump failed: ${e.message}`);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} suggested=${suggested} ratios=${JSON.stringify({
|
|
541
|
-
tilled: +ratios.tilled.toFixed(3),
|
|
542
|
-
planted: +ratios.planted.toFixed(3),
|
|
543
|
-
unknown: +ratios.unknown.toFixed(3),
|
|
544
|
-
})}`);
|
|
545
|
-
if (dbg) {
|
|
546
|
-
const tileSummary = (analysis.cells || [])
|
|
547
|
-
.map(c => `r${c.row + 1}c${c.col + 1}:${c.state}:${c.confidence}`)
|
|
548
|
-
.join(' | ');
|
|
549
|
-
LOG.info(`[farm:phase-image] debug dir=${dbg.dir} source=${dbg.sourcePath} manifest=${dbg.manifestPath} tiles=${dbg.tileCount}`);
|
|
550
|
-
LOG.info(`[farm:phase-image] tile-summary ${tileSummary}`);
|
|
551
|
-
}
|
|
525
|
+
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} suggested=${suggested}`);
|
|
552
526
|
return suggested;
|
|
553
527
|
} catch (e) {
|
|
554
528
|
LOG.warn(`[farm:phase-image] inference failed: ${e.message}`);
|
|
555
529
|
return null;
|
|
556
530
|
}
|
|
557
531
|
}
|
|
532
|
+
*/
|
|
558
533
|
|
|
559
534
|
function chooseSeedOption(menu) {
|
|
560
535
|
const options = (menu?.options || []).filter(o => o && o.value);
|
|
@@ -642,11 +617,11 @@ async function ensurePlantSeedSelected({ response, waitForDankMemer, channel })
|
|
|
642
617
|
}
|
|
643
618
|
}
|
|
644
619
|
|
|
620
|
+
/* DISABLED — vision-based phase scoring
|
|
645
621
|
async function capturePhaseVisionScore({ msg, actionName, phaseTag }) {
|
|
646
622
|
if (!msg || !['hoe', 'water', 'plant', 'harvest'].includes(actionName)) {
|
|
647
623
|
return null;
|
|
648
624
|
}
|
|
649
|
-
|
|
650
625
|
const check = await analyzeFarmImageForAction(msg, actionName);
|
|
651
626
|
const s = check?.score;
|
|
652
627
|
if (s) {
|
|
@@ -656,6 +631,7 @@ async function capturePhaseVisionScore({ msg, actionName, phaseTag }) {
|
|
|
656
631
|
}
|
|
657
632
|
return check;
|
|
658
633
|
}
|
|
634
|
+
*/
|
|
659
635
|
|
|
660
636
|
function mergeBuyPlan(plan) {
|
|
661
637
|
const m = new Map();
|
|
@@ -1115,10 +1091,22 @@ async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs
|
|
|
1115
1091
|
// Determine the next action to take from the manage menu, given what the
|
|
1116
1092
|
// farm image vision says about the current state.
|
|
1117
1093
|
// Returns { action, button, reason } where action is null when the cycle is done.
|
|
1118
|
-
async function findNextFarmActionFromManage(msg, text, currentAction,
|
|
1094
|
+
async function findNextFarmActionFromManage(msg, text, currentAction, forcedNextAction) {
|
|
1119
1095
|
const btns = getAllButtons(msg).filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1120
1096
|
const allBtns = getAllButtons(msg); // include disabled for state detection
|
|
1121
|
-
|
|
1097
|
+
// If no non-utility buttons remain, check if any action tabs exist.
|
|
1098
|
+
// This handles confirmation screens where Manage/Rename/Change Skin are all
|
|
1099
|
+
// filtered as nav buttons — the manage menu's action tabs (Hoe/Water/Plant/Harvest)
|
|
1100
|
+
// may still be accessible directly or via the manage button.
|
|
1101
|
+
if (btns.length === 0) {
|
|
1102
|
+
const actionTabs = allBtns.filter(b => !b.disabled && hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz']));
|
|
1103
|
+
if (actionTabs.length > 0) {
|
|
1104
|
+
// Action tabs exist but are buried behind a confirmation/nav screen.
|
|
1105
|
+
// The caller will need to navigate back to the manage menu.
|
|
1106
|
+
return { action: null, button: null, reason: 'confirmation-screen', actionTabs };
|
|
1107
|
+
}
|
|
1108
|
+
return { action: null, button: null, reason: 'no-buttons' };
|
|
1109
|
+
}
|
|
1122
1110
|
|
|
1123
1111
|
const managedActions = getManageActionButtons(msg);
|
|
1124
1112
|
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
@@ -1149,6 +1137,7 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1149
1137
|
if (btn) return { action: 'harvest', button: btn, reason: 'harvest-ready-text' };
|
|
1150
1138
|
}
|
|
1151
1139
|
|
|
1140
|
+
/* DISABLED — vision-based image analysis routing
|
|
1152
1141
|
// Use image vision ratios if available.
|
|
1153
1142
|
if (imageAnalysis) {
|
|
1154
1143
|
const slots = Math.max(1, (imageAnalysis.rows || 3) * (imageAnalysis.cols || 3));
|
|
@@ -1157,7 +1146,6 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1157
1146
|
planted: (imageAnalysis.counts?.planted || 0) / slots,
|
|
1158
1147
|
unknown: (imageAnalysis.counts?.unknown || 0) / slots,
|
|
1159
1148
|
};
|
|
1160
|
-
|
|
1161
1149
|
// Phase-aware selection: pick the earliest incomplete phase (3-state model).
|
|
1162
1150
|
// Harvest when planted >= 30%.
|
|
1163
1151
|
if (ratios.planted >= 0.30) {
|
|
@@ -1180,6 +1168,7 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1180
1168
|
if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},p=${ratios.planted.toFixed(2)})` };
|
|
1181
1169
|
}
|
|
1182
1170
|
}
|
|
1171
|
+
*/
|
|
1183
1172
|
|
|
1184
1173
|
// Disabled-all detection: if the "All" for a phase is already disabled,
|
|
1185
1174
|
// that phase is done — move to the next one.
|
|
@@ -1238,6 +1227,12 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1238
1227
|
await humanDelay(80, 220);
|
|
1239
1228
|
|
|
1240
1229
|
const baseline = brief(getFullText(response), 400);
|
|
1230
|
+
// NOTE: We do NOT register an ephemeral listener here.
|
|
1231
|
+
// The outer listener (registered by the caller) captures ALL ephemerals
|
|
1232
|
+
// including harvest results. Registering a second listener would clear
|
|
1233
|
+
// the listeners array after the first fires, preventing the outer
|
|
1234
|
+
// listener from seeing the ephemeral.
|
|
1235
|
+
|
|
1241
1236
|
let clicked;
|
|
1242
1237
|
try {
|
|
1243
1238
|
clicked = await safeClickButton(response, confirmBtn);
|
|
@@ -1353,12 +1348,22 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1353
1348
|
if (textHasHarvestReady) LOG.info(`[farm] DETECTED harvest-ready in text (growReadySec=${growReadySec})`);
|
|
1354
1349
|
if (growReadySec > 0) LOG.info(`[farm] GROW-QUEUE DEBUG: lower=${lower.slice(0, 400)}`);
|
|
1355
1350
|
LOG.info(`[farm] grow-ready parse=${growReadySec == null ? 'none' : `${growReadySec}s`} redis_recovery=${redisRecoveryMs != null ? `${redisRecoveryMs}ms` : 'n/a'}`);
|
|
1356
|
-
if (!
|
|
1357
|
-
|
|
1358
|
-
//
|
|
1359
|
-
// This
|
|
1360
|
-
|
|
1361
|
-
|
|
1351
|
+
if (!redisRecovered && growReadySec && growReadySec > 20) {
|
|
1352
|
+
// Grow timer is active — schedule re-check based on how close crops are.
|
|
1353
|
+
// Graduated wait: short when nearly ready, longer when far out.
|
|
1354
|
+
// This avoids spamming Discord with farm commands when crops have hours left.
|
|
1355
|
+
let waitSec;
|
|
1356
|
+
if (growReadySec <= 300) {
|
|
1357
|
+
// < 5 min: check every minute (crops nearly ready)
|
|
1358
|
+
waitSec = 60;
|
|
1359
|
+
} else if (growReadySec <= 3600) {
|
|
1360
|
+
// 5-60 min: check every 5 minutes
|
|
1361
|
+
waitSec = 300;
|
|
1362
|
+
} else {
|
|
1363
|
+
// > 1 hour: check every 15 minutes (long grow, no rush)
|
|
1364
|
+
waitSec = 900;
|
|
1365
|
+
}
|
|
1366
|
+
LOG.info(`[farm] crops growing (~${Math.ceil(growReadySec / 60)}m remaining); re-checking in ${waitSec / 60}m`);
|
|
1362
1367
|
return { result: `farm grow queue (${Math.ceil(growReadySec / 60)}m)`, coins: 0, nextCooldownSec: waitSec, skipReason: 'farm_grow_queue' };
|
|
1363
1368
|
}
|
|
1364
1369
|
|
|
@@ -1395,19 +1400,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1395
1400
|
}
|
|
1396
1401
|
}
|
|
1397
1402
|
|
|
1398
|
-
// ── Initial image analysis to bootstrap cycle entry ────────────────────────
|
|
1399
|
-
let initialAnalysis = null;
|
|
1400
|
-
try {
|
|
1401
|
-
const url = extractFarmImageUrl(response);
|
|
1402
|
-
if (url) {
|
|
1403
|
-
const buf = await downloadImage(url);
|
|
1404
|
-
const grid = await analyzeFarmGrid(buf);
|
|
1405
|
-
initialAnalysis = grid;
|
|
1406
|
-
LOG.info(`[farm] initial image grid=${gridToString(grid)} counts=${JSON.stringify(grid.counts)} conf=${grid.avgConfidence}`);
|
|
1407
|
-
}
|
|
1408
|
-
} catch (e) {
|
|
1409
|
-
LOG.info(`[farm] initial image analysis failed: ${e.message}`);
|
|
1410
|
-
}
|
|
1411
1403
|
|
|
1412
1404
|
// ── Cycle loop: hoe → water → plant → harvest ─────────────────────────────
|
|
1413
1405
|
let cycleDepth = 0;
|
|
@@ -1416,11 +1408,15 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1416
1408
|
let actionsTaken = 0; // Track how many farm actions were actually executed
|
|
1417
1409
|
let forcedNextAction = null; // When advancing a phase, force the next action check
|
|
1418
1410
|
let lastApplyResp = null; // Track the last apply response for coin/cooldown parsing
|
|
1411
|
+
let lastRejectedAction = null; // Track last rejected action to break empty-farm loops
|
|
1412
|
+
let lastRejectedAction2 = null; // Track previous rejected action to detect 3-in-a-row loops
|
|
1419
1413
|
|
|
1420
1414
|
while (cycleDepth < 5) {
|
|
1415
|
+
// Reset per-cycle state
|
|
1416
|
+
let justRejected = false;
|
|
1421
1417
|
let actionResult;
|
|
1422
1418
|
try {
|
|
1423
|
-
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction,
|
|
1419
|
+
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, forcedNextAction);
|
|
1424
1420
|
} catch (e) {
|
|
1425
1421
|
LOG.warn(`[farm:cycle] findNextFarmActionFromManage error: ${e.message}`);
|
|
1426
1422
|
break;
|
|
@@ -1429,6 +1425,23 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1429
1425
|
forcedNextAction = null;
|
|
1430
1426
|
|
|
1431
1427
|
if (!action || !button) {
|
|
1428
|
+
if (reason === 'confirmation-screen') {
|
|
1429
|
+
// We're on a confirmation/nav screen with no actionable buttons visible.
|
|
1430
|
+
// Re-enter the manage menu to get action tabs back.
|
|
1431
|
+
LOG.info(`[farm:cycle:${cycleDepth}] confirmation screen — re-entering manage menu`);
|
|
1432
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1433
|
+
if (mb) {
|
|
1434
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: `farm-cycle-confirm-manage` });
|
|
1435
|
+
if (mr) {
|
|
1436
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1437
|
+
cycleResponse = mr;
|
|
1438
|
+
text = getFullText(cycleResponse);
|
|
1439
|
+
clean = brief(text, 600);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
await sleep(300);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1432
1445
|
LOG.info(`[farm:cycle:${cycleDepth}] no action found (reason=${reason}) — breaking cycle`);
|
|
1433
1446
|
break;
|
|
1434
1447
|
}
|
|
@@ -1490,39 +1503,48 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1490
1503
|
label.includes('plant all') || label.includes('harvest all') || label.includes('confirm')) && b.disabled;
|
|
1491
1504
|
});
|
|
1492
1505
|
if (disabledAllBtn) {
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1506
|
+
// Farm state check: "Hoe All" disabled could mean (a) no Hoe item OR (b) no tiles to work.
|
|
1507
|
+
// Check the farm text — if the farm is empty, there's nothing to hoe/water/plant,
|
|
1508
|
+
// so buying is pointless. Skip buy and treat as a no-op phase.
|
|
1509
|
+
const farmEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(clean);
|
|
1510
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1511
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1512
|
+
if (farmEmpty) {
|
|
1513
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled but farm is empty — skipping ${action}, advancing to ${nextPhase || 'done'}`);
|
|
1514
|
+
lastAction = action;
|
|
1515
|
+
forcedNextAction = nextPhase;
|
|
1516
|
+
cycleDepth++;
|
|
1517
|
+
if (!nextPhase) break;
|
|
1518
|
+
await sleep(300);
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
// Farm has tiles but all-btn disabled — try to buy the item.
|
|
1495
1522
|
const ITEM_FOR_ACTION = { hoe: 'Hoe', water: 'Watering Can', plant: 'Seeds' };
|
|
1496
1523
|
const missingItem = ITEM_FOR_ACTION[action];
|
|
1497
1524
|
if (missingItem) {
|
|
1498
|
-
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled — trying to buy ${missingItem}`);
|
|
1525
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (farm not empty) — trying to buy ${missingItem}`);
|
|
1499
1526
|
const bought = await tryBuyFarmItem({ missing: missingItem, channel, waitForDankMemer, client });
|
|
1500
1527
|
if (bought.ok) {
|
|
1501
1528
|
LOG.success(`[farm:cycle:${cycleDepth}] Bought ${bought.itemName} — retrying ${action}`);
|
|
1502
1529
|
await sleep(1200);
|
|
1503
|
-
//
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
text = getFullText(cycleResponse);
|
|
1510
|
-
clean = brief(text, 600);
|
|
1511
|
-
initialAnalysis = null;
|
|
1530
|
+
// Refresh the manage menu to pick up the new button state
|
|
1531
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1532
|
+
if (mb) {
|
|
1533
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-buy-retry-manage' });
|
|
1534
|
+
if (mr) { if (isCV2(mr)) await ensureCV2(mr); cycleResponse = mr; text = getFullText(cycleResponse); clean = brief(text, 600); }
|
|
1535
|
+
}
|
|
1512
1536
|
continue;
|
|
1513
1537
|
} else {
|
|
1514
1538
|
LOG.warn(`[farm:cycle:${cycleDepth}] Could not buy ${missingItem} — skipping to next phase`);
|
|
1515
1539
|
}
|
|
1516
1540
|
}
|
|
1517
|
-
|
|
1518
|
-
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1541
|
+
|
|
1519
1542
|
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (no-op) — next=${nextPhase || 'done'}`);
|
|
1520
1543
|
actionsTaken++;
|
|
1521
1544
|
lastAction = action;
|
|
1522
1545
|
forcedNextAction = nextPhase;
|
|
1523
1546
|
cycleDepth++;
|
|
1524
1547
|
if (!nextPhase) break;
|
|
1525
|
-
initialAnalysis = null;
|
|
1526
1548
|
await sleep(300);
|
|
1527
1549
|
continue;
|
|
1528
1550
|
}
|
|
@@ -1539,20 +1561,146 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1539
1561
|
if (!lastApplyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
|
|
1540
1562
|
|
|
1541
1563
|
// Step 5: advance past any confirmation screens back to the manage menu
|
|
1564
|
+
// Register the ephemeral listener BEFORE advancePastConfirmation so we capture
|
|
1565
|
+
// the confirmation ephemeral (e.g., harvest "You harvested:") which arrives
|
|
1566
|
+
// asynchronously after the HTTP response.
|
|
1567
|
+
let confirmEphemeral = null;
|
|
1568
|
+
rawLogger.onNextEphemeral((parsed) => {
|
|
1569
|
+
confirmEphemeral = parsed;
|
|
1570
|
+
});
|
|
1542
1571
|
cycleResponse = await advancePastConfirmation(lastApplyResp, waitForDankMemer);
|
|
1543
1572
|
if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
|
|
1544
1573
|
if (isCV2(cycleResponse)) await ensureCV2(cycleResponse);
|
|
1574
|
+
// Ephemeral fires asynchronously after HTTP response — wait briefly for it to arrive
|
|
1575
|
+
// before the harvest block checks cycleResponse._capturedEphemeral.
|
|
1576
|
+
await sleep(200);
|
|
1577
|
+
if (confirmEphemeral) cycleResponse._capturedEphemeral = confirmEphemeral;
|
|
1578
|
+
else if (cycleResponse?._capturedEphemeral) { /* already set by advancePastConfirmation */ }
|
|
1545
1579
|
actionsTaken++;
|
|
1546
1580
|
lastAction = action;
|
|
1547
1581
|
text = getFullText(cycleResponse);
|
|
1548
1582
|
clean = brief(text, 600);
|
|
1549
1583
|
logFarmState('after-confirm', cycleResponse);
|
|
1550
1584
|
|
|
1551
|
-
// ──
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1585
|
+
// ── Rejection ephemeral: detect "wrong phase" errors and cascade ─────────────
|
|
1586
|
+
// advancePastConfirmation registers no ephemeral listener (confirmation ephemerals
|
|
1587
|
+
// are captured by the outer listener below). The action's ephemeral (from clicking
|
|
1588
|
+
// the action tab) remains on lastApplyResp._capturedEphemeral.
|
|
1555
1589
|
{
|
|
1590
|
+
const ephem = lastApplyResp?._capturedEphemeral;
|
|
1591
|
+
const ephemText = (ephem?.cv2Text || ephem?.allText || '').toLowerCase();
|
|
1592
|
+
|
|
1593
|
+
// Generic "nothing to X" no-op: when an action completes but has nothing to act on,
|
|
1594
|
+
// cascade forward without treating it as a rejection. Also use this as a safety break
|
|
1595
|
+
// when the same action keeps getting rejected (stuck in a loop).
|
|
1596
|
+
if (
|
|
1597
|
+
// "Nothing to water/hoe/plant/harvest" — action succeeded but was a no-op
|
|
1598
|
+
/nothing to (water|hoe|plant|harvest)|no crops? (to |to )?(water|harvest)|0 x [a-z]|nothing to do/i.test(ephemText) ||
|
|
1599
|
+
// Safety: if the same action was rejected 3+ cycles in a row, something is stuck — break
|
|
1600
|
+
(lastRejectedAction === action && lastRejectedAction2 === action)
|
|
1601
|
+
) {
|
|
1602
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] ${action} is a no-op or stuck loop (${ephemText.slice(0, 80)}) — cascading to next phase`);
|
|
1603
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1604
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1605
|
+
lastRejectedAction2 = lastRejectedAction;
|
|
1606
|
+
lastRejectedAction = action;
|
|
1607
|
+
forcedNextAction = nextPhase;
|
|
1608
|
+
lastAction = action;
|
|
1609
|
+
justRejected = true;
|
|
1610
|
+
cycleDepth++;
|
|
1611
|
+
if (!nextPhase) { forcedNextAction = null; break; }
|
|
1612
|
+
await reenterManage();
|
|
1613
|
+
await sleep(300);
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Helper: re-enter the manage menu from the farm view so the next
|
|
1618
|
+
// findNextFarmActionFromManage iteration has buttons to work with.
|
|
1619
|
+
async function reenterManage() {
|
|
1620
|
+
const allBtns = getAllButtons(cycleResponse);
|
|
1621
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1622
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] findFarmButton result: ${mb ? mb.label + ' / ' + mb.customId : 'NULL'}`);
|
|
1623
|
+
if (!mb) {
|
|
1624
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reenter] no Manage button found! available btns: ${allBtns.map(b => b.label).join(', ')}`);
|
|
1625
|
+
return false;
|
|
1626
|
+
}
|
|
1627
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: `farm-cycle-reject-manage` });
|
|
1628
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] clickAndCapture result: ${mr ? 'got response' : 'NULL'}`);
|
|
1629
|
+
if (!mr) return false;
|
|
1630
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1631
|
+
cycleResponse = mr;
|
|
1632
|
+
const postBtns = getAllButtons(cycleResponse);
|
|
1633
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] post-manage btns: ${postBtns.length}, ${postBtns.map(b => b.label).join(', ')}`);
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// "You can only use a Hoe on empty tiles or after a harvest" → skip to water
|
|
1638
|
+
if (action === 'hoe' && /can only use.*hoe.*empty|only use.*hoe.*empty/.test(ephemText)) {
|
|
1639
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Hoe rejected: tiles not empty — cascading to water`);
|
|
1640
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1641
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1642
|
+
lastRejectedAction2 = lastRejectedAction;
|
|
1643
|
+
lastRejectedAction = action;
|
|
1644
|
+
forcedNextAction = nextPhase;
|
|
1645
|
+
lastAction = action;
|
|
1646
|
+
justRejected = true;
|
|
1647
|
+
cycleDepth++;
|
|
1648
|
+
if (!nextPhase) break;
|
|
1649
|
+
await reenterManage();
|
|
1650
|
+
await sleep(300);
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// "You can only use a Watering Can on tilled tiles" (not hoed) → cascade forward to plant
|
|
1655
|
+
// Do NOT cascade back to hoe — that creates an infinite hoe↔water oscillation.
|
|
1656
|
+
if (action === 'water' && (ephemText.includes('can only use') || ephemText.includes('can only water'))) {
|
|
1657
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Water rejected (${ephemText.slice(0, 100)}) — cascading to plant`);
|
|
1658
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1659
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1660
|
+
lastRejectedAction2 = lastRejectedAction;
|
|
1661
|
+
lastRejectedAction = action;
|
|
1662
|
+
forcedNextAction = nextPhase;
|
|
1663
|
+
lastAction = action;
|
|
1664
|
+
justRejected = true;
|
|
1665
|
+
// Break oscillation guard: if we bounced back here from plant, stop.
|
|
1666
|
+
if (lastRejectedAction2 === 'plant' && lastRejectedAction === 'water') {
|
|
1667
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] hoe→water→plant oscillation detected — breaking`);
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
cycleDepth++;
|
|
1671
|
+
if (!nextPhase) break;
|
|
1672
|
+
await reenterManage();
|
|
1673
|
+
await sleep(300);
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// "You can only plant seeds on an empty tile that is tilled and watered" → cascade to harvest
|
|
1678
|
+
// Do NOT cascade back to hoe — that creates an infinite hoe↔water oscillation.
|
|
1679
|
+
if (action === 'plant' && /can only plant|can only use.*seed|plant.*not.*water/.test(ephemText)) {
|
|
1680
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Plant rejected (${ephemText.slice(0, 100)}) — cascading to harvest`);
|
|
1681
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1682
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1683
|
+
lastRejectedAction2 = lastRejectedAction;
|
|
1684
|
+
lastRejectedAction = action;
|
|
1685
|
+
forcedNextAction = nextPhase;
|
|
1686
|
+
lastAction = action;
|
|
1687
|
+
justRejected = true;
|
|
1688
|
+
cycleDepth++;
|
|
1689
|
+
if (!nextPhase) {
|
|
1690
|
+
// Plant is last phase — advance to harvest in next cycle.
|
|
1691
|
+
await sleep(300);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
await reenterManage();
|
|
1695
|
+
await sleep(300);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// ── Farm text detection: determine next action from farm state ─────────────────
|
|
1701
|
+
// Skip entirely if a rejection just fired — the rejection cascade handles the
|
|
1702
|
+
// next action; farm-text should not override forcedNextAction.
|
|
1703
|
+
if (!justRejected) {
|
|
1556
1704
|
const postBtns = getAllButtons(cycleResponse);
|
|
1557
1705
|
const postActionBtns = postBtns.filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1558
1706
|
const postHasActionTabs = postActionBtns.some(b => hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz']));
|
|
@@ -1562,6 +1710,14 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1562
1710
|
const isEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(postLower) && !hasSeedsReady && !hasHarvestReady;
|
|
1563
1711
|
if (!postHasActionTabs && !isEmpty) {
|
|
1564
1712
|
// Farm has crops (planted/growing). Force harvest.
|
|
1713
|
+
// BUT: if the current action was 'harvest' and we're on a planted confirmation
|
|
1714
|
+
// screen (no action tabs, not empty), it means nothing was harvested.
|
|
1715
|
+
// Breaking here lets the grow queue handle the planted crops.
|
|
1716
|
+
const justHarvested = (action === 'harvest');
|
|
1717
|
+
if (justHarvested) {
|
|
1718
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: nothing harvested (planted confirmation) — breaking, grow queue will handle`);
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1565
1721
|
// cycleResponse is still the farm view (no action tabs) after advancePastConfirmation.
|
|
1566
1722
|
// Re-enter the manage menu so findNextFarmActionFromManage has buttons to work with.
|
|
1567
1723
|
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
@@ -1578,12 +1734,13 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1578
1734
|
forcedNextAction = 'harvest';
|
|
1579
1735
|
lastAction = null;
|
|
1580
1736
|
cycleDepth++;
|
|
1581
|
-
initialAnalysis = null;
|
|
1582
1737
|
await sleep(300);
|
|
1583
1738
|
continue;
|
|
1584
1739
|
}
|
|
1585
1740
|
if (!postHasActionTabs && isEmpty) {
|
|
1586
|
-
// Farm is empty after the action. Re-enter manage menu
|
|
1741
|
+
// Farm is empty after the action. Re-enter manage menu and check if there's
|
|
1742
|
+
// actually anything to work. If all "All" buttons are still disabled, the
|
|
1743
|
+
// farm is truly empty — nothing to hoe/water/plant, break the cycle.
|
|
1587
1744
|
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1588
1745
|
if (mb) {
|
|
1589
1746
|
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: re-entering manage menu (empty farm)`);
|
|
@@ -1595,10 +1752,19 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1595
1752
|
clean = brief(text, 600);
|
|
1596
1753
|
}
|
|
1597
1754
|
}
|
|
1755
|
+
// Double-check: after re-entering manage, if hoe/water/plant all-btn are all still
|
|
1756
|
+
// disabled, the farm is truly empty — no tiles to work. Break the cycle.
|
|
1757
|
+
const allBtnsNow = getAllButtons(cycleResponse);
|
|
1758
|
+
const hoeAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('hoe all'));
|
|
1759
|
+
const waterAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('water all'));
|
|
1760
|
+
const plantAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('plant all'));
|
|
1761
|
+
if (!hoeAllEnabled && !waterAllEnabled && !plantAllEnabled) {
|
|
1762
|
+
LOG.info(`[farm:cycle:${cycleDepth}] all action all-btns still disabled after manage re-entry — farm is truly empty, breaking`);
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1598
1765
|
forcedNextAction = 'hoe';
|
|
1599
1766
|
lastAction = null;
|
|
1600
1767
|
cycleDepth++;
|
|
1601
|
-
initialAnalysis = null;
|
|
1602
1768
|
await sleep(300);
|
|
1603
1769
|
continue;
|
|
1604
1770
|
}
|
|
@@ -1607,22 +1773,18 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1607
1773
|
|
|
1608
1774
|
// ── Harvest ephemeral: parse "You harvested: Nothing" to detect no-op ──────────
|
|
1609
1775
|
if (action === 'harvest') {
|
|
1610
|
-
const ephemeral = lastApplyResp?._capturedEphemeral;
|
|
1776
|
+
const ephemeral = cycleResponse?._capturedEphemeral || lastApplyResp?._capturedEphemeral;
|
|
1611
1777
|
if (ephemeral) {
|
|
1612
1778
|
const cv2t = ephemeral.cv2Text || '';
|
|
1613
1779
|
const allt = ephemeral.allText || '';
|
|
1614
1780
|
const harvestText = cv2t || allt;
|
|
1615
|
-
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] "${
|
|
1781
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] cv2Text="${cv2t.slice(0,200)}" allText="${allt.slice(0,200)}"`);
|
|
1616
1782
|
// If nothing was harvested, cells are now empty (post-harvest debris).
|
|
1617
|
-
//
|
|
1618
|
-
if (/nothing|0\s*x\s*\w|no\s+crops|\bnone\b/i.test(harvestText)) {
|
|
1619
|
-
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Nothing harvested —
|
|
1620
|
-
forcedNextAction =
|
|
1621
|
-
|
|
1622
|
-
cycleDepth++;
|
|
1623
|
-
initialAnalysis = null;
|
|
1624
|
-
await sleep(300);
|
|
1625
|
-
continue;
|
|
1783
|
+
// Clear forcedNextAction and break — farm is done.
|
|
1784
|
+
if (/nothing|0\s*x\s*\w|no\s+crops|\bnone\b|^you harvested\s*$/im.test(harvestText) || (/\byou harvested\b/i.test(harvestText) && /\b(nothing|none)\b/i.test(harvestText))) {
|
|
1785
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Nothing harvested — farm cycle complete`);
|
|
1786
|
+
forcedNextAction = null;
|
|
1787
|
+
break;
|
|
1626
1788
|
}
|
|
1627
1789
|
// Something was actually harvested — parse what and log it.
|
|
1628
1790
|
const itemMatches = [...harvestText.matchAll(/-?\s*(\d+)\s*x?\s*([A-Za-z]+)/g)];
|
|
@@ -1660,6 +1822,9 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1660
1822
|
logFarmState('after-plant-back', cycleResponse);
|
|
1661
1823
|
}
|
|
1662
1824
|
// Plant succeeded — advance to harvest.
|
|
1825
|
+
// cycleResponse is now the farm view (after clicking Back).
|
|
1826
|
+
// Re-enter manage so findNextFarmActionFromManage has buttons on the next loop.
|
|
1827
|
+
await reenterManage();
|
|
1663
1828
|
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1664
1829
|
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1665
1830
|
LOG.info(`[farm:cycle:${cycleDepth}] plant done, advancing to ${nextPhase || 'done'}`);
|
|
@@ -1667,7 +1832,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1667
1832
|
if (!nextPhase) break;
|
|
1668
1833
|
cycleDepth++;
|
|
1669
1834
|
lastAction = nextPhase;
|
|
1670
|
-
initialAnalysis = null;
|
|
1671
1835
|
await sleep(400);
|
|
1672
1836
|
continue;
|
|
1673
1837
|
}
|
|
@@ -1679,7 +1843,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1679
1843
|
if (!nextPhase) break;
|
|
1680
1844
|
cycleDepth++;
|
|
1681
1845
|
lastAction = nextPhase;
|
|
1682
|
-
initialAnalysis = null;
|
|
1683
1846
|
await sleep(300);
|
|
1684
1847
|
continue;
|
|
1685
1848
|
}
|
|
@@ -1687,22 +1850,11 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1687
1850
|
clean = brief(text, 600);
|
|
1688
1851
|
logFarmState('after-confirm', cycleResponse);
|
|
1689
1852
|
|
|
1690
|
-
// Step 6:
|
|
1691
|
-
let imgAnalysis = null;
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
const buf = await downloadImage(url);
|
|
1696
|
-
imgAnalysis = await analyzeFarmGrid(buf);
|
|
1697
|
-
LOG.info(`[farm:cycle:${cycleDepth}:img] grid=${gridToString(imgAnalysis)} counts=${JSON.stringify(imgAnalysis.counts)} conf=${imgAnalysis.avgConfidence}`);
|
|
1698
|
-
}
|
|
1699
|
-
} catch (e) {
|
|
1700
|
-
LOG.info(`[farm:cycle:${cycleDepth}:img] failed: ${e.message}`);
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
const scoreResult = await capturePhaseVisionScore({ msg: cycleResponse, actionName: action, phaseTag: `farm-cycle-${action}` });
|
|
1704
|
-
const score = scoreResult?.score || null;
|
|
1705
|
-
const nextAction = inferNextActionFromScore(action, score);
|
|
1853
|
+
// Step 6: NO image analysis — rely purely on ephemeral + text for next action decision
|
|
1854
|
+
// let imgAnalysis = null; // DISABLED vision
|
|
1855
|
+
// const scoreResult = await capturePhaseVisionScore({ msg: cycleResponse, actionName: action, phaseTag: `farm-cycle-${action}` });
|
|
1856
|
+
// const nextAction = inferNextActionFromScore(action, scoreResult?.score || null); // DISABLED
|
|
1857
|
+
const nextAction = null; // DISABLED — purely ephemeral-based flow
|
|
1706
1858
|
|
|
1707
1859
|
// Collect all error/rejection text sources:
|
|
1708
1860
|
// 1. Extra follow-up messages from farm actions
|
|
@@ -1754,7 +1906,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1754
1906
|
forcedNextAction = 'harvest';
|
|
1755
1907
|
cycleDepth++;
|
|
1756
1908
|
lastAction = 'water';
|
|
1757
|
-
|
|
1909
|
+
|
|
1758
1910
|
await sleep(300);
|
|
1759
1911
|
continue;
|
|
1760
1912
|
}
|
|
@@ -1766,7 +1918,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1766
1918
|
LOG.info(`[farm:cycle:${cycleDepth}] next action = ${nextAction} (after ${action})`);
|
|
1767
1919
|
lastAction = action;
|
|
1768
1920
|
cycleDepth++;
|
|
1769
|
-
|
|
1921
|
+
|
|
1770
1922
|
await sleep(300);
|
|
1771
1923
|
}
|
|
1772
1924
|
|
|
@@ -1806,7 +1958,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1806
1958
|
}
|
|
1807
1959
|
}
|
|
1808
1960
|
|
|
1809
|
-
|
|
1961
|
+
|
|
1810
1962
|
|
|
1811
1963
|
if (coins > 0) {
|
|
1812
1964
|
LOG.coin(`[farm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
package/lib/grinder.js
CHANGED
|
@@ -3214,10 +3214,12 @@ async function start(apiKey, apiUrl) {
|
|
|
3214
3214
|
loginLines.push(` ${'─'.repeat(loginVis)}`);
|
|
3215
3215
|
for (const l of loginLines) console.log(l);
|
|
3216
3216
|
|
|
3217
|
-
// Dynamically capture the starting row of the login table via DSR
|
|
3217
|
+
// Dynamically capture the starting row of the login table via DSR.
|
|
3218
|
+
// Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
|
|
3219
|
+
// of the visible "@MARKER@@" text portion, which was causing the DSR
|
|
3220
|
+
// response to be swallowed or delayed.
|
|
3218
3221
|
let loginBaseRow = 1;
|
|
3219
3222
|
const captureLoginRow = () => new Promise(resolve => {
|
|
3220
|
-
process.stdout.write(MARKER);
|
|
3221
3223
|
const chunks = [];
|
|
3222
3224
|
const handler = (chunk) => {
|
|
3223
3225
|
chunks.push(chunk);
|
|
@@ -3230,6 +3232,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3230
3232
|
}
|
|
3231
3233
|
};
|
|
3232
3234
|
process.stdin.on('data', handler);
|
|
3235
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3236
|
+
process.stderr.write(MARKER);
|
|
3233
3237
|
setTimeout(resolve, 50);
|
|
3234
3238
|
});
|
|
3235
3239
|
await captureLoginRow();
|
|
@@ -3331,7 +3335,7 @@ async function start(apiKey, apiUrl) {
|
|
|
3331
3335
|
const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
|
|
3332
3336
|
|
|
3333
3337
|
// Print a unique marker, query its position, then overwrite it with the table
|
|
3334
|
-
|
|
3338
|
+
// Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
|
|
3335
3339
|
let invBaseRow = 1;
|
|
3336
3340
|
const captureRow = () => new Promise(resolve => {
|
|
3337
3341
|
const chunks = [];
|
|
@@ -3346,6 +3350,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3346
3350
|
}
|
|
3347
3351
|
};
|
|
3348
3352
|
process.stdin.on('data', handler);
|
|
3353
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3354
|
+
process.stderr.write(MARKER);
|
|
3349
3355
|
setTimeout(resolve, 50);
|
|
3350
3356
|
});
|
|
3351
3357
|
await captureRow();
|
|
@@ -3414,7 +3420,7 @@ async function start(apiKey, apiUrl) {
|
|
|
3414
3420
|
const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
|
|
3415
3421
|
|
|
3416
3422
|
// Capture starting row for balance phase
|
|
3417
|
-
|
|
3423
|
+
// Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
|
|
3418
3424
|
let balBaseRow = 1;
|
|
3419
3425
|
const balCaptureRow = () => new Promise(resolve => {
|
|
3420
3426
|
const chunks = [];
|
|
@@ -3429,6 +3435,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3429
3435
|
}
|
|
3430
3436
|
};
|
|
3431
3437
|
process.stdin.on('data', handler);
|
|
3438
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3439
|
+
process.stderr.write(MARKER);
|
|
3432
3440
|
setTimeout(resolve, 50);
|
|
3433
3441
|
});
|
|
3434
3442
|
await balCaptureRow();
|