dankgrinder 7.7.0 → 7.11.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 +235 -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,14 @@ 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
|
|
1419
1412
|
|
|
1420
1413
|
while (cycleDepth < 5) {
|
|
1414
|
+
// Reset per-cycle state
|
|
1415
|
+
let justRejected = false;
|
|
1421
1416
|
let actionResult;
|
|
1422
1417
|
try {
|
|
1423
|
-
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction,
|
|
1418
|
+
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, forcedNextAction);
|
|
1424
1419
|
} catch (e) {
|
|
1425
1420
|
LOG.warn(`[farm:cycle] findNextFarmActionFromManage error: ${e.message}`);
|
|
1426
1421
|
break;
|
|
@@ -1429,6 +1424,23 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1429
1424
|
forcedNextAction = null;
|
|
1430
1425
|
|
|
1431
1426
|
if (!action || !button) {
|
|
1427
|
+
if (reason === 'confirmation-screen') {
|
|
1428
|
+
// We're on a confirmation/nav screen with no actionable buttons visible.
|
|
1429
|
+
// Re-enter the manage menu to get action tabs back.
|
|
1430
|
+
LOG.info(`[farm:cycle:${cycleDepth}] confirmation screen — re-entering manage menu`);
|
|
1431
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1432
|
+
if (mb) {
|
|
1433
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: `farm-cycle-confirm-manage` });
|
|
1434
|
+
if (mr) {
|
|
1435
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1436
|
+
cycleResponse = mr;
|
|
1437
|
+
text = getFullText(cycleResponse);
|
|
1438
|
+
clean = brief(text, 600);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
await sleep(300);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1432
1444
|
LOG.info(`[farm:cycle:${cycleDepth}] no action found (reason=${reason}) — breaking cycle`);
|
|
1433
1445
|
break;
|
|
1434
1446
|
}
|
|
@@ -1490,39 +1502,48 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1490
1502
|
label.includes('plant all') || label.includes('harvest all') || label.includes('confirm')) && b.disabled;
|
|
1491
1503
|
});
|
|
1492
1504
|
if (disabledAllBtn) {
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1505
|
+
// Farm state check: "Hoe All" disabled could mean (a) no Hoe item OR (b) no tiles to work.
|
|
1506
|
+
// Check the farm text — if the farm is empty, there's nothing to hoe/water/plant,
|
|
1507
|
+
// so buying is pointless. Skip buy and treat as a no-op phase.
|
|
1508
|
+
const farmEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(clean);
|
|
1509
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1510
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1511
|
+
if (farmEmpty) {
|
|
1512
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled but farm is empty — skipping ${action}, advancing to ${nextPhase || 'done'}`);
|
|
1513
|
+
lastAction = action;
|
|
1514
|
+
forcedNextAction = nextPhase;
|
|
1515
|
+
cycleDepth++;
|
|
1516
|
+
if (!nextPhase) break;
|
|
1517
|
+
await sleep(300);
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
// Farm has tiles but all-btn disabled — try to buy the item.
|
|
1495
1521
|
const ITEM_FOR_ACTION = { hoe: 'Hoe', water: 'Watering Can', plant: 'Seeds' };
|
|
1496
1522
|
const missingItem = ITEM_FOR_ACTION[action];
|
|
1497
1523
|
if (missingItem) {
|
|
1498
|
-
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled — trying to buy ${missingItem}`);
|
|
1524
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (farm not empty) — trying to buy ${missingItem}`);
|
|
1499
1525
|
const bought = await tryBuyFarmItem({ missing: missingItem, channel, waitForDankMemer, client });
|
|
1500
1526
|
if (bought.ok) {
|
|
1501
1527
|
LOG.success(`[farm:cycle:${cycleDepth}] Bought ${bought.itemName} — retrying ${action}`);
|
|
1502
1528
|
await sleep(1200);
|
|
1503
|
-
//
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
text = getFullText(cycleResponse);
|
|
1510
|
-
clean = brief(text, 600);
|
|
1511
|
-
initialAnalysis = null;
|
|
1529
|
+
// Refresh the manage menu to pick up the new button state
|
|
1530
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1531
|
+
if (mb) {
|
|
1532
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-buy-retry-manage' });
|
|
1533
|
+
if (mr) { if (isCV2(mr)) await ensureCV2(mr); cycleResponse = mr; text = getFullText(cycleResponse); clean = brief(text, 600); }
|
|
1534
|
+
}
|
|
1512
1535
|
continue;
|
|
1513
1536
|
} else {
|
|
1514
1537
|
LOG.warn(`[farm:cycle:${cycleDepth}] Could not buy ${missingItem} — skipping to next phase`);
|
|
1515
1538
|
}
|
|
1516
1539
|
}
|
|
1517
|
-
|
|
1518
|
-
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1540
|
+
|
|
1519
1541
|
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (no-op) — next=${nextPhase || 'done'}`);
|
|
1520
1542
|
actionsTaken++;
|
|
1521
1543
|
lastAction = action;
|
|
1522
1544
|
forcedNextAction = nextPhase;
|
|
1523
1545
|
cycleDepth++;
|
|
1524
1546
|
if (!nextPhase) break;
|
|
1525
|
-
initialAnalysis = null;
|
|
1526
1547
|
await sleep(300);
|
|
1527
1548
|
continue;
|
|
1528
1549
|
}
|
|
@@ -1539,20 +1560,116 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1539
1560
|
if (!lastApplyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
|
|
1540
1561
|
|
|
1541
1562
|
// Step 5: advance past any confirmation screens back to the manage menu
|
|
1563
|
+
// Register the ephemeral listener BEFORE advancePastConfirmation so we capture
|
|
1564
|
+
// the confirmation ephemeral (e.g., harvest "You harvested:") which arrives
|
|
1565
|
+
// asynchronously after the HTTP response.
|
|
1566
|
+
let confirmEphemeral = null;
|
|
1567
|
+
rawLogger.onNextEphemeral((parsed) => {
|
|
1568
|
+
confirmEphemeral = parsed;
|
|
1569
|
+
});
|
|
1542
1570
|
cycleResponse = await advancePastConfirmation(lastApplyResp, waitForDankMemer);
|
|
1543
1571
|
if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
|
|
1544
1572
|
if (isCV2(cycleResponse)) await ensureCV2(cycleResponse);
|
|
1573
|
+
// Ephemeral fires asynchronously after HTTP response — wait briefly for it to arrive
|
|
1574
|
+
// before the harvest block checks cycleResponse._capturedEphemeral.
|
|
1575
|
+
await sleep(200);
|
|
1576
|
+
if (confirmEphemeral) cycleResponse._capturedEphemeral = confirmEphemeral;
|
|
1577
|
+
else if (cycleResponse?._capturedEphemeral) { /* already set by advancePastConfirmation */ }
|
|
1545
1578
|
actionsTaken++;
|
|
1546
1579
|
lastAction = action;
|
|
1547
1580
|
text = getFullText(cycleResponse);
|
|
1548
1581
|
clean = brief(text, 600);
|
|
1549
1582
|
logFarmState('after-confirm', cycleResponse);
|
|
1550
1583
|
|
|
1551
|
-
// ──
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1584
|
+
// ── Rejection ephemeral: detect "wrong phase" errors and cascade ─────────────
|
|
1585
|
+
// advancePastConfirmation registers no ephemeral listener (confirmation ephemerals
|
|
1586
|
+
// are captured by the outer listener below). The action's ephemeral (from clicking
|
|
1587
|
+
// the action tab) remains on lastApplyResp._capturedEphemeral.
|
|
1555
1588
|
{
|
|
1589
|
+
const ephem = lastApplyResp?._capturedEphemeral;
|
|
1590
|
+
const ephemText = (ephem?.cv2Text || ephem?.allText || '').toLowerCase();
|
|
1591
|
+
|
|
1592
|
+
// Helper: re-enter the manage menu from the farm view so the next
|
|
1593
|
+
// findNextFarmActionFromManage iteration has buttons to work with.
|
|
1594
|
+
async function reenterManage() {
|
|
1595
|
+
const allBtns = getAllButtons(cycleResponse);
|
|
1596
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1597
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] findFarmButton result: ${mb ? mb.label + ' / ' + mb.customId : 'NULL'}`);
|
|
1598
|
+
if (!mb) {
|
|
1599
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reenter] no Manage button found! available btns: ${allBtns.map(b => b.label).join(', ')}`);
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: `farm-cycle-reject-manage` });
|
|
1603
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] clickAndCapture result: ${mr ? 'got response' : 'NULL'}`);
|
|
1604
|
+
if (!mr) return false;
|
|
1605
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1606
|
+
cycleResponse = mr;
|
|
1607
|
+
const postBtns = getAllButtons(cycleResponse);
|
|
1608
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] post-manage btns: ${postBtns.length}, ${postBtns.map(b => b.label).join(', ')}`);
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// "You can only use a Hoe on empty tiles or after a harvest" → skip to water
|
|
1613
|
+
if (action === 'hoe' && /can only use.*hoe.*empty|only use.*hoe.*empty/.test(ephemText)) {
|
|
1614
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Hoe rejected: tiles not empty — cascading to water`);
|
|
1615
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1616
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1617
|
+
forcedNextAction = nextPhase;
|
|
1618
|
+
lastAction = action;
|
|
1619
|
+
justRejected = true;
|
|
1620
|
+
cycleDepth++;
|
|
1621
|
+
if (!nextPhase) break;
|
|
1622
|
+
await reenterManage();
|
|
1623
|
+
await sleep(300);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// "You can only use a Watering Can on tilled tiles" (not hoed) → cascade forward to plant
|
|
1628
|
+
// Do NOT cascade back to hoe — that creates an infinite hoe↔water oscillation.
|
|
1629
|
+
if (action === 'water' && (ephemText.includes('can only use') || ephemText.includes('can only water'))) {
|
|
1630
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Water rejected (${ephemText.slice(0, 100)}) — cascading to plant`);
|
|
1631
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1632
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1633
|
+
forcedNextAction = nextPhase;
|
|
1634
|
+
lastAction = action;
|
|
1635
|
+
justRejected = true;
|
|
1636
|
+
// Break oscillation guard: if we bounced back here from plant, stop.
|
|
1637
|
+
if (lastRejectedAction === 'plant' && action === 'water') {
|
|
1638
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] hoe→water→plant oscillation detected — breaking`);
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
cycleDepth++;
|
|
1642
|
+
if (!nextPhase) break;
|
|
1643
|
+
await reenterManage();
|
|
1644
|
+
await sleep(300);
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// "You can only plant seeds on an empty tile that is tilled and watered" → cascade to harvest
|
|
1649
|
+
// Do NOT cascade back to hoe — that creates an infinite hoe↔water oscillation.
|
|
1650
|
+
if (action === 'plant' && /can only plant|can only use.*seed|plant.*not.*water/.test(ephemText)) {
|
|
1651
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Plant rejected (${ephemText.slice(0, 100)}) — cascading to harvest`);
|
|
1652
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1653
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1654
|
+
forcedNextAction = nextPhase;
|
|
1655
|
+
lastAction = action;
|
|
1656
|
+
justRejected = true;
|
|
1657
|
+
cycleDepth++;
|
|
1658
|
+
if (!nextPhase) {
|
|
1659
|
+
// Plant is last phase — advance to harvest in next cycle.
|
|
1660
|
+
await sleep(300);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
await reenterManage();
|
|
1664
|
+
await sleep(300);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// ── Farm text detection: determine next action from farm state ─────────────────
|
|
1670
|
+
// Skip entirely if a rejection just fired — the rejection cascade handles the
|
|
1671
|
+
// next action; farm-text should not override forcedNextAction.
|
|
1672
|
+
if (!justRejected) {
|
|
1556
1673
|
const postBtns = getAllButtons(cycleResponse);
|
|
1557
1674
|
const postActionBtns = postBtns.filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1558
1675
|
const postHasActionTabs = postActionBtns.some(b => hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz']));
|
|
@@ -1562,6 +1679,14 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1562
1679
|
const isEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(postLower) && !hasSeedsReady && !hasHarvestReady;
|
|
1563
1680
|
if (!postHasActionTabs && !isEmpty) {
|
|
1564
1681
|
// Farm has crops (planted/growing). Force harvest.
|
|
1682
|
+
// BUT: if the current action was 'harvest' and we're on a planted confirmation
|
|
1683
|
+
// screen (no action tabs, not empty), it means nothing was harvested.
|
|
1684
|
+
// Breaking here lets the grow queue handle the planted crops.
|
|
1685
|
+
const justHarvested = (action === 'harvest');
|
|
1686
|
+
if (justHarvested) {
|
|
1687
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: nothing harvested (planted confirmation) — breaking, grow queue will handle`);
|
|
1688
|
+
break;
|
|
1689
|
+
}
|
|
1565
1690
|
// cycleResponse is still the farm view (no action tabs) after advancePastConfirmation.
|
|
1566
1691
|
// Re-enter the manage menu so findNextFarmActionFromManage has buttons to work with.
|
|
1567
1692
|
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
@@ -1578,12 +1703,13 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1578
1703
|
forcedNextAction = 'harvest';
|
|
1579
1704
|
lastAction = null;
|
|
1580
1705
|
cycleDepth++;
|
|
1581
|
-
initialAnalysis = null;
|
|
1582
1706
|
await sleep(300);
|
|
1583
1707
|
continue;
|
|
1584
1708
|
}
|
|
1585
1709
|
if (!postHasActionTabs && isEmpty) {
|
|
1586
|
-
// Farm is empty after the action. Re-enter manage menu
|
|
1710
|
+
// Farm is empty after the action. Re-enter manage menu and check if there's
|
|
1711
|
+
// actually anything to work. If all "All" buttons are still disabled, the
|
|
1712
|
+
// farm is truly empty — nothing to hoe/water/plant, break the cycle.
|
|
1587
1713
|
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1588
1714
|
if (mb) {
|
|
1589
1715
|
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: re-entering manage menu (empty farm)`);
|
|
@@ -1595,10 +1721,19 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1595
1721
|
clean = brief(text, 600);
|
|
1596
1722
|
}
|
|
1597
1723
|
}
|
|
1724
|
+
// Double-check: after re-entering manage, if hoe/water/plant all-btn are all still
|
|
1725
|
+
// disabled, the farm is truly empty — no tiles to work. Break the cycle.
|
|
1726
|
+
const allBtnsNow = getAllButtons(cycleResponse);
|
|
1727
|
+
const hoeAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('hoe all'));
|
|
1728
|
+
const waterAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('water all'));
|
|
1729
|
+
const plantAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('plant all'));
|
|
1730
|
+
if (!hoeAllEnabled && !waterAllEnabled && !plantAllEnabled) {
|
|
1731
|
+
LOG.info(`[farm:cycle:${cycleDepth}] all action all-btns still disabled after manage re-entry — farm is truly empty, breaking`);
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1598
1734
|
forcedNextAction = 'hoe';
|
|
1599
1735
|
lastAction = null;
|
|
1600
1736
|
cycleDepth++;
|
|
1601
|
-
initialAnalysis = null;
|
|
1602
1737
|
await sleep(300);
|
|
1603
1738
|
continue;
|
|
1604
1739
|
}
|
|
@@ -1607,22 +1742,18 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1607
1742
|
|
|
1608
1743
|
// ── Harvest ephemeral: parse "You harvested: Nothing" to detect no-op ──────────
|
|
1609
1744
|
if (action === 'harvest') {
|
|
1610
|
-
const ephemeral = lastApplyResp?._capturedEphemeral;
|
|
1745
|
+
const ephemeral = cycleResponse?._capturedEphemeral || lastApplyResp?._capturedEphemeral;
|
|
1611
1746
|
if (ephemeral) {
|
|
1612
1747
|
const cv2t = ephemeral.cv2Text || '';
|
|
1613
1748
|
const allt = ephemeral.allText || '';
|
|
1614
1749
|
const harvestText = cv2t || allt;
|
|
1615
|
-
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] "${
|
|
1750
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] cv2Text="${cv2t.slice(0,200)}" allText="${allt.slice(0,200)}"`);
|
|
1616
1751
|
// 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;
|
|
1752
|
+
// Clear forcedNextAction and break — farm is done.
|
|
1753
|
+
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))) {
|
|
1754
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Nothing harvested — farm cycle complete`);
|
|
1755
|
+
forcedNextAction = null;
|
|
1756
|
+
break;
|
|
1626
1757
|
}
|
|
1627
1758
|
// Something was actually harvested — parse what and log it.
|
|
1628
1759
|
const itemMatches = [...harvestText.matchAll(/-?\s*(\d+)\s*x?\s*([A-Za-z]+)/g)];
|
|
@@ -1660,6 +1791,9 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1660
1791
|
logFarmState('after-plant-back', cycleResponse);
|
|
1661
1792
|
}
|
|
1662
1793
|
// Plant succeeded — advance to harvest.
|
|
1794
|
+
// cycleResponse is now the farm view (after clicking Back).
|
|
1795
|
+
// Re-enter manage so findNextFarmActionFromManage has buttons on the next loop.
|
|
1796
|
+
await reenterManage();
|
|
1663
1797
|
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1664
1798
|
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1665
1799
|
LOG.info(`[farm:cycle:${cycleDepth}] plant done, advancing to ${nextPhase || 'done'}`);
|
|
@@ -1667,7 +1801,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1667
1801
|
if (!nextPhase) break;
|
|
1668
1802
|
cycleDepth++;
|
|
1669
1803
|
lastAction = nextPhase;
|
|
1670
|
-
initialAnalysis = null;
|
|
1671
1804
|
await sleep(400);
|
|
1672
1805
|
continue;
|
|
1673
1806
|
}
|
|
@@ -1679,7 +1812,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1679
1812
|
if (!nextPhase) break;
|
|
1680
1813
|
cycleDepth++;
|
|
1681
1814
|
lastAction = nextPhase;
|
|
1682
|
-
initialAnalysis = null;
|
|
1683
1815
|
await sleep(300);
|
|
1684
1816
|
continue;
|
|
1685
1817
|
}
|
|
@@ -1687,22 +1819,11 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1687
1819
|
clean = brief(text, 600);
|
|
1688
1820
|
logFarmState('after-confirm', cycleResponse);
|
|
1689
1821
|
|
|
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);
|
|
1822
|
+
// Step 6: NO image analysis — rely purely on ephemeral + text for next action decision
|
|
1823
|
+
// let imgAnalysis = null; // DISABLED vision
|
|
1824
|
+
// const scoreResult = await capturePhaseVisionScore({ msg: cycleResponse, actionName: action, phaseTag: `farm-cycle-${action}` });
|
|
1825
|
+
// const nextAction = inferNextActionFromScore(action, scoreResult?.score || null); // DISABLED
|
|
1826
|
+
const nextAction = null; // DISABLED — purely ephemeral-based flow
|
|
1706
1827
|
|
|
1707
1828
|
// Collect all error/rejection text sources:
|
|
1708
1829
|
// 1. Extra follow-up messages from farm actions
|
|
@@ -1754,7 +1875,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1754
1875
|
forcedNextAction = 'harvest';
|
|
1755
1876
|
cycleDepth++;
|
|
1756
1877
|
lastAction = 'water';
|
|
1757
|
-
|
|
1878
|
+
|
|
1758
1879
|
await sleep(300);
|
|
1759
1880
|
continue;
|
|
1760
1881
|
}
|
|
@@ -1766,7 +1887,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1766
1887
|
LOG.info(`[farm:cycle:${cycleDepth}] next action = ${nextAction} (after ${action})`);
|
|
1767
1888
|
lastAction = action;
|
|
1768
1889
|
cycleDepth++;
|
|
1769
|
-
|
|
1890
|
+
|
|
1770
1891
|
await sleep(300);
|
|
1771
1892
|
}
|
|
1772
1893
|
|
|
@@ -1806,7 +1927,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
|
|
|
1806
1927
|
}
|
|
1807
1928
|
}
|
|
1808
1929
|
|
|
1809
|
-
|
|
1930
|
+
|
|
1810
1931
|
|
|
1811
1932
|
if (coins > 0) {
|
|
1812
1933
|
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();
|