browser-pilot 0.0.7 → 0.0.8
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/dist/actions.cjs +20 -3
- package/dist/actions.d.cts +4 -4
- package/dist/actions.d.ts +4 -4
- package/dist/actions.mjs +1 -1
- package/dist/browser.cjs +357 -34
- package/dist/browser.d.cts +7 -3
- package/dist/browser.d.ts +7 -3
- package/dist/browser.mjs +6 -5
- package/dist/{chunk-PCNEJAJ7.mjs → chunk-JN44FHTK.mjs} +331 -36
- package/dist/{chunk-6RB3GKQP.mjs → chunk-ZTQ37YQT.mjs} +35 -3
- package/dist/cli.cjs +1654 -69
- package/dist/cli.mjs +1292 -28
- package/dist/index.cjs +357 -34
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/{types-TVlTA7nH.d.cts → types-DklIxnbO.d.cts} +37 -3
- package/dist/{types-CbdmaocU.d.ts → types-Pv8KzZ6l.d.ts} +37 -3
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -209,6 +209,18 @@ EXAMPLES
|
|
|
209
209
|
{"action":"click","selector":"#delete-btn"},
|
|
210
210
|
{"action":"wait","selector":"#success-message","waitFor":"visible"}
|
|
211
211
|
]'
|
|
212
|
+
|
|
213
|
+
DEBUGGING
|
|
214
|
+
When actions fail, use these diagnostic tools:
|
|
215
|
+
|
|
216
|
+
bp diagnose '#selector' # Why can't this be found?
|
|
217
|
+
bp diagnose '#selector' --json # Machine-readable output
|
|
218
|
+
bp snapshot --diff prev.json # What changed on the page?
|
|
219
|
+
bp snapshot --inspect # Visual ref labels on page
|
|
220
|
+
bp list -s <id> --log-tail 10 # Recent command history
|
|
221
|
+
|
|
222
|
+
Failure hints are included in error output when element not found.
|
|
223
|
+
Use --json output for detailed hints with alternative selectors.
|
|
212
224
|
`;
|
|
213
225
|
async function actionsCommand() {
|
|
214
226
|
console.log(ACTIONS_HELP);
|
|
@@ -219,20 +231,20 @@ var import_node_os = require("os");
|
|
|
219
231
|
var import_node_path = require("path");
|
|
220
232
|
var SESSION_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".browser-pilot", "sessions");
|
|
221
233
|
async function ensureSessionDir() {
|
|
222
|
-
const
|
|
223
|
-
await
|
|
234
|
+
const fs3 = await import("fs/promises");
|
|
235
|
+
await fs3.mkdir(SESSION_DIR, { recursive: true });
|
|
224
236
|
}
|
|
225
237
|
async function saveSession(session) {
|
|
226
238
|
await ensureSessionDir();
|
|
227
|
-
const
|
|
239
|
+
const fs3 = await import("fs/promises");
|
|
228
240
|
const filePath = (0, import_node_path.join)(SESSION_DIR, `${session.id}.json`);
|
|
229
|
-
await
|
|
241
|
+
await fs3.writeFile(filePath, JSON.stringify(session, null, 2));
|
|
230
242
|
}
|
|
231
243
|
async function loadSession(id) {
|
|
232
|
-
const
|
|
244
|
+
const fs3 = await import("fs/promises");
|
|
233
245
|
const filePath = (0, import_node_path.join)(SESSION_DIR, `${id}.json`);
|
|
234
246
|
try {
|
|
235
|
-
const content = await
|
|
247
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
236
248
|
return JSON.parse(content);
|
|
237
249
|
} catch (error) {
|
|
238
250
|
if (error.code === "ENOENT") {
|
|
@@ -254,10 +266,10 @@ async function updateSession(id, updates) {
|
|
|
254
266
|
return updated;
|
|
255
267
|
}
|
|
256
268
|
async function deleteSession(id) {
|
|
257
|
-
const
|
|
269
|
+
const fs3 = await import("fs/promises");
|
|
258
270
|
const filePath = (0, import_node_path.join)(SESSION_DIR, `${id}.json`);
|
|
259
271
|
try {
|
|
260
|
-
await
|
|
272
|
+
await fs3.unlink(filePath);
|
|
261
273
|
} catch (error) {
|
|
262
274
|
if (error.code !== "ENOENT") {
|
|
263
275
|
throw error;
|
|
@@ -266,14 +278,14 @@ async function deleteSession(id) {
|
|
|
266
278
|
}
|
|
267
279
|
async function listSessions() {
|
|
268
280
|
await ensureSessionDir();
|
|
269
|
-
const
|
|
281
|
+
const fs3 = await import("fs/promises");
|
|
270
282
|
try {
|
|
271
|
-
const files = await
|
|
283
|
+
const files = await fs3.readdir(SESSION_DIR);
|
|
272
284
|
const sessions = [];
|
|
273
285
|
for (const file of files) {
|
|
274
286
|
if (file.endsWith(".json")) {
|
|
275
287
|
try {
|
|
276
|
-
const content = await
|
|
288
|
+
const content = await fs3.readFile((0, import_node_path.join)(SESSION_DIR, file), "utf-8");
|
|
277
289
|
sessions.push(JSON.parse(content));
|
|
278
290
|
} catch {
|
|
279
291
|
}
|
|
@@ -350,6 +362,25 @@ async function cleanCommand(args, globalOptions) {
|
|
|
350
362
|
);
|
|
351
363
|
}
|
|
352
364
|
|
|
365
|
+
// src/browser/types.ts
|
|
366
|
+
var ElementNotFoundError = class extends Error {
|
|
367
|
+
selectors;
|
|
368
|
+
hints;
|
|
369
|
+
constructor(selectors, hints) {
|
|
370
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
371
|
+
super(`Element not found: ${selectorList.join(", ")}`);
|
|
372
|
+
this.name = "ElementNotFoundError";
|
|
373
|
+
this.selectors = selectorList;
|
|
374
|
+
this.hints = hints;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var TimeoutError = class extends Error {
|
|
378
|
+
constructor(message = "Operation timed out") {
|
|
379
|
+
super(message);
|
|
380
|
+
this.name = "TimeoutError";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
353
384
|
// src/actions/executor.ts
|
|
354
385
|
var DEFAULT_TIMEOUT = 3e4;
|
|
355
386
|
var BatchExecutor = class {
|
|
@@ -381,13 +412,15 @@ var BatchExecutor = class {
|
|
|
381
412
|
});
|
|
382
413
|
} catch (error) {
|
|
383
414
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
415
|
+
const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
|
|
384
416
|
results.push({
|
|
385
417
|
index: i,
|
|
386
418
|
action: step.action,
|
|
387
419
|
selector: step.selector,
|
|
388
420
|
success: false,
|
|
389
421
|
durationMs: Date.now() - stepStart,
|
|
390
|
-
error: errorMessage
|
|
422
|
+
error: errorMessage,
|
|
423
|
+
hints
|
|
391
424
|
});
|
|
392
425
|
if (onFail === "stop" && !step.optional) {
|
|
393
426
|
return {
|
|
@@ -522,7 +555,7 @@ var BatchExecutor = class {
|
|
|
522
555
|
case "wait": {
|
|
523
556
|
if (!step.selector && !step.waitFor) {
|
|
524
557
|
const delay = step.timeout ?? 1e3;
|
|
525
|
-
await new Promise((
|
|
558
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
526
559
|
return {};
|
|
527
560
|
}
|
|
528
561
|
if (step.waitFor === "navigation") {
|
|
@@ -583,10 +616,12 @@ var BatchExecutor = class {
|
|
|
583
616
|
}
|
|
584
617
|
}
|
|
585
618
|
/**
|
|
586
|
-
* Get the
|
|
587
|
-
*
|
|
619
|
+
* Get the actual selector that matched the element.
|
|
620
|
+
* Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
|
|
588
621
|
*/
|
|
589
622
|
getUsedSelector(selector) {
|
|
623
|
+
const matched = this.page.getLastMatchedSelector();
|
|
624
|
+
if (matched) return matched;
|
|
590
625
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
591
626
|
}
|
|
592
627
|
};
|
|
@@ -612,7 +647,7 @@ var CDPError = class extends Error {
|
|
|
612
647
|
// src/cdp/transport.ts
|
|
613
648
|
function createTransport(wsUrl, options = {}) {
|
|
614
649
|
const { timeout = 3e4 } = options;
|
|
615
|
-
return new Promise((
|
|
650
|
+
return new Promise((resolve2, reject) => {
|
|
616
651
|
const timeoutId = setTimeout(() => {
|
|
617
652
|
reject(new Error(`WebSocket connection timeout after ${timeout}ms`));
|
|
618
653
|
}, timeout);
|
|
@@ -657,7 +692,7 @@ function createTransport(wsUrl, options = {}) {
|
|
|
657
692
|
errorHandlers.push(handler);
|
|
658
693
|
}
|
|
659
694
|
};
|
|
660
|
-
|
|
695
|
+
resolve2(transport);
|
|
661
696
|
});
|
|
662
697
|
ws.addEventListener("message", (event) => {
|
|
663
698
|
const data = typeof event.data === "string" ? event.data : String(event.data);
|
|
@@ -781,13 +816,13 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
781
816
|
if (debug) {
|
|
782
817
|
console.log("[CDP] -->", message.slice(0, 500));
|
|
783
818
|
}
|
|
784
|
-
return new Promise((
|
|
819
|
+
return new Promise((resolve2, reject) => {
|
|
785
820
|
const timer = setTimeout(() => {
|
|
786
821
|
pending.delete(id);
|
|
787
822
|
reject(new Error(`CDP command ${method} timed out after ${timeout}ms`));
|
|
788
823
|
}, timeout);
|
|
789
824
|
pending.set(id, {
|
|
790
|
-
resolve,
|
|
825
|
+
resolve: resolve2,
|
|
791
826
|
reject,
|
|
792
827
|
method,
|
|
793
828
|
timer
|
|
@@ -1285,7 +1320,7 @@ async function isElementAttached(cdp, selector, contextId) {
|
|
|
1285
1320
|
return result.result.value === true;
|
|
1286
1321
|
}
|
|
1287
1322
|
function sleep(ms) {
|
|
1288
|
-
return new Promise((
|
|
1323
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1289
1324
|
}
|
|
1290
1325
|
async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
1291
1326
|
const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
|
|
@@ -1332,14 +1367,14 @@ async function waitForNavigation(cdp, options = {}) {
|
|
|
1332
1367
|
} catch {
|
|
1333
1368
|
startUrl = "";
|
|
1334
1369
|
}
|
|
1335
|
-
return new Promise((
|
|
1370
|
+
return new Promise((resolve2) => {
|
|
1336
1371
|
let resolved = false;
|
|
1337
1372
|
const cleanup = [];
|
|
1338
1373
|
const done = (success) => {
|
|
1339
1374
|
if (resolved) return;
|
|
1340
1375
|
resolved = true;
|
|
1341
1376
|
for (const fn of cleanup) fn();
|
|
1342
|
-
|
|
1377
|
+
resolve2({ success, waitedMs: Date.now() - startTime });
|
|
1343
1378
|
};
|
|
1344
1379
|
const timer = setTimeout(() => done(false), timeout);
|
|
1345
1380
|
cleanup.push(() => clearTimeout(timer));
|
|
@@ -1380,19 +1415,19 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
1380
1415
|
const { timeout = 3e4, idleTime = 500 } = options;
|
|
1381
1416
|
const startTime = Date.now();
|
|
1382
1417
|
await cdp.send("Network.enable");
|
|
1383
|
-
return new Promise((
|
|
1418
|
+
return new Promise((resolve2) => {
|
|
1384
1419
|
let inFlight = 0;
|
|
1385
1420
|
let idleTimer = null;
|
|
1386
1421
|
const timeoutTimer = setTimeout(() => {
|
|
1387
1422
|
cleanup();
|
|
1388
|
-
|
|
1423
|
+
resolve2({ success: false, waitedMs: Date.now() - startTime });
|
|
1389
1424
|
}, timeout);
|
|
1390
1425
|
const checkIdle = () => {
|
|
1391
1426
|
if (inFlight === 0) {
|
|
1392
1427
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1393
1428
|
idleTimer = setTimeout(() => {
|
|
1394
1429
|
cleanup();
|
|
1395
|
-
|
|
1430
|
+
resolve2({ success: true, waitedMs: Date.now() - startTime });
|
|
1396
1431
|
}, idleTime);
|
|
1397
1432
|
}
|
|
1398
1433
|
};
|
|
@@ -1421,27 +1456,256 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
1421
1456
|
});
|
|
1422
1457
|
}
|
|
1423
1458
|
|
|
1424
|
-
// src/browser/
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1459
|
+
// src/browser/fuzzy-match.ts
|
|
1460
|
+
function jaroWinkler(a, b) {
|
|
1461
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
1462
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1463
|
+
if (a === b) return 1;
|
|
1464
|
+
const s1 = a.toLowerCase();
|
|
1465
|
+
const s2 = b.toLowerCase();
|
|
1466
|
+
const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
|
|
1467
|
+
const s1Matches = new Array(s1.length).fill(false);
|
|
1468
|
+
const s2Matches = new Array(s2.length).fill(false);
|
|
1469
|
+
let matches = 0;
|
|
1470
|
+
let transpositions = 0;
|
|
1471
|
+
for (let i = 0; i < s1.length; i++) {
|
|
1472
|
+
const start = Math.max(0, i - matchWindow);
|
|
1473
|
+
const end = Math.min(i + matchWindow + 1, s2.length);
|
|
1474
|
+
for (let j = start; j < end; j++) {
|
|
1475
|
+
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
1476
|
+
s1Matches[i] = true;
|
|
1477
|
+
s2Matches[j] = true;
|
|
1478
|
+
matches++;
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (matches === 0) return 0;
|
|
1483
|
+
let k = 0;
|
|
1484
|
+
for (let i = 0; i < s1.length; i++) {
|
|
1485
|
+
if (!s1Matches[i]) continue;
|
|
1486
|
+
while (!s2Matches[k]) k++;
|
|
1487
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
1488
|
+
k++;
|
|
1489
|
+
}
|
|
1490
|
+
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
1491
|
+
let prefix = 0;
|
|
1492
|
+
for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
|
|
1493
|
+
if (s1[i] === s2[i]) {
|
|
1494
|
+
prefix++;
|
|
1495
|
+
} else {
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1432
1498
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1499
|
+
const WINKLER_SCALING = 0.1;
|
|
1500
|
+
return jaro + prefix * WINKLER_SCALING * (1 - jaro);
|
|
1501
|
+
}
|
|
1502
|
+
function stringSimilarity(a, b) {
|
|
1503
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1504
|
+
const lowerA = a.toLowerCase();
|
|
1505
|
+
const lowerB = b.toLowerCase();
|
|
1506
|
+
if (lowerA === lowerB) return 1;
|
|
1507
|
+
const jw = jaroWinkler(a, b);
|
|
1508
|
+
let containsBonus = 0;
|
|
1509
|
+
if (lowerB.includes(lowerA)) {
|
|
1510
|
+
containsBonus = 0.2;
|
|
1511
|
+
} else if (lowerA.includes(lowerB)) {
|
|
1512
|
+
containsBonus = 0.1;
|
|
1513
|
+
}
|
|
1514
|
+
return Math.min(1, jw + containsBonus);
|
|
1515
|
+
}
|
|
1516
|
+
function scoreElement(query, element) {
|
|
1517
|
+
const lowerQuery = query.toLowerCase();
|
|
1518
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
1519
|
+
let nameScore = 0;
|
|
1520
|
+
if (element.name) {
|
|
1521
|
+
const lowerName = element.name.toLowerCase();
|
|
1522
|
+
if (lowerName === lowerQuery) {
|
|
1523
|
+
nameScore = 1;
|
|
1524
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
1525
|
+
nameScore = 0.8;
|
|
1526
|
+
} else if (words.length > 0) {
|
|
1527
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
1528
|
+
nameScore = matchedWords.length / words.length * 0.7;
|
|
1529
|
+
} else {
|
|
1530
|
+
nameScore = stringSimilarity(query, element.name) * 0.6;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
let roleScore = 0;
|
|
1534
|
+
const lowerRole = element.role.toLowerCase();
|
|
1535
|
+
if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
|
|
1536
|
+
roleScore = 0.3;
|
|
1537
|
+
} else if (words.some((w) => lowerRole.includes(w))) {
|
|
1538
|
+
roleScore = 0.2;
|
|
1438
1539
|
}
|
|
1540
|
+
let selectorScore = 0;
|
|
1541
|
+
const lowerSelector = element.selector.toLowerCase();
|
|
1542
|
+
if (words.some((w) => lowerSelector.includes(w))) {
|
|
1543
|
+
selectorScore = 0.2;
|
|
1544
|
+
}
|
|
1545
|
+
const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
|
|
1546
|
+
return totalScore;
|
|
1547
|
+
}
|
|
1548
|
+
function explainMatch(query, element, score) {
|
|
1549
|
+
const reasons = [];
|
|
1550
|
+
const lowerQuery = query.toLowerCase();
|
|
1551
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
1552
|
+
if (element.name) {
|
|
1553
|
+
const lowerName = element.name.toLowerCase();
|
|
1554
|
+
if (lowerName === lowerQuery) {
|
|
1555
|
+
reasons.push("exact name match");
|
|
1556
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
1557
|
+
reasons.push("name contains query");
|
|
1558
|
+
} else if (words.some((w) => lowerName.includes(w))) {
|
|
1559
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
1560
|
+
reasons.push(`name contains: ${matchedWords.join(", ")}`);
|
|
1561
|
+
} else if (stringSimilarity(query, element.name) > 0.5) {
|
|
1562
|
+
reasons.push("similar name");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const lowerRole = element.role.toLowerCase();
|
|
1566
|
+
if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
|
|
1567
|
+
reasons.push(`role: ${element.role}`);
|
|
1568
|
+
}
|
|
1569
|
+
if (words.some((w) => element.selector.toLowerCase().includes(w))) {
|
|
1570
|
+
reasons.push("selector match");
|
|
1571
|
+
}
|
|
1572
|
+
if (reasons.length === 0) {
|
|
1573
|
+
reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
|
|
1574
|
+
}
|
|
1575
|
+
return reasons.join(", ");
|
|
1576
|
+
}
|
|
1577
|
+
function fuzzyMatchElements(query, elements, maxResults = 5) {
|
|
1578
|
+
if (!query || query.length === 0) {
|
|
1579
|
+
return [];
|
|
1580
|
+
}
|
|
1581
|
+
const THRESHOLD = 0.3;
|
|
1582
|
+
const scored = elements.map((element) => ({
|
|
1583
|
+
element,
|
|
1584
|
+
score: scoreElement(query, element)
|
|
1585
|
+
}));
|
|
1586
|
+
return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
|
|
1587
|
+
element: s.element,
|
|
1588
|
+
score: s.score,
|
|
1589
|
+
matchReason: explainMatch(query, s.element, s.score)
|
|
1590
|
+
}));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/browser/hint-generator.ts
|
|
1594
|
+
var ACTION_ROLE_MAP = {
|
|
1595
|
+
click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
|
|
1596
|
+
fill: ["textbox", "searchbox", "textarea"],
|
|
1597
|
+
type: ["textbox", "searchbox", "textarea"],
|
|
1598
|
+
submit: ["button", "form"],
|
|
1599
|
+
select: ["combobox", "listbox", "option"],
|
|
1600
|
+
check: ["checkbox", "radio", "switch"],
|
|
1601
|
+
uncheck: ["checkbox", "switch"],
|
|
1602
|
+
focus: [],
|
|
1603
|
+
// Any focusable element
|
|
1604
|
+
hover: [],
|
|
1605
|
+
// Any element
|
|
1606
|
+
clear: ["textbox", "searchbox", "textarea"]
|
|
1439
1607
|
};
|
|
1608
|
+
function extractIntent(selectors) {
|
|
1609
|
+
const patterns = [];
|
|
1610
|
+
let text = "";
|
|
1611
|
+
for (const selector of selectors) {
|
|
1612
|
+
if (selector.startsWith("ref:")) {
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
1616
|
+
if (idMatch) {
|
|
1617
|
+
patterns.push(idMatch[1]);
|
|
1618
|
+
}
|
|
1619
|
+
const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
|
|
1620
|
+
if (ariaMatch) {
|
|
1621
|
+
patterns.push(ariaMatch[1]);
|
|
1622
|
+
}
|
|
1623
|
+
const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
|
|
1624
|
+
if (testidMatch) {
|
|
1625
|
+
patterns.push(testidMatch[1]);
|
|
1626
|
+
}
|
|
1627
|
+
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
1628
|
+
if (classMatch) {
|
|
1629
|
+
patterns.push(classMatch[1]);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
patterns.sort((a, b) => b.length - a.length);
|
|
1633
|
+
text = patterns[0] ?? selectors[0] ?? "";
|
|
1634
|
+
return { text, patterns };
|
|
1635
|
+
}
|
|
1636
|
+
function getHintType(selector) {
|
|
1637
|
+
if (selector.startsWith("ref:")) return "ref";
|
|
1638
|
+
if (selector.includes("data-testid")) return "testid";
|
|
1639
|
+
if (selector.includes("aria-label")) return "aria";
|
|
1640
|
+
if (selector.startsWith("#")) return "id";
|
|
1641
|
+
return "css";
|
|
1642
|
+
}
|
|
1643
|
+
function getConfidence(score) {
|
|
1644
|
+
if (score >= 0.8) return "high";
|
|
1645
|
+
if (score >= 0.5) return "medium";
|
|
1646
|
+
return "low";
|
|
1647
|
+
}
|
|
1648
|
+
function diversifyHints(candidates, maxHints) {
|
|
1649
|
+
const hints = [];
|
|
1650
|
+
const usedTypes = /* @__PURE__ */ new Set();
|
|
1651
|
+
for (const candidate of candidates) {
|
|
1652
|
+
if (hints.length >= maxHints) break;
|
|
1653
|
+
const refSelector = `ref:${candidate.element.ref}`;
|
|
1654
|
+
const hintType = getHintType(refSelector);
|
|
1655
|
+
if (!usedTypes.has(hintType)) {
|
|
1656
|
+
hints.push({
|
|
1657
|
+
selector: refSelector,
|
|
1658
|
+
reason: candidate.matchReason,
|
|
1659
|
+
confidence: getConfidence(candidate.score),
|
|
1660
|
+
element: {
|
|
1661
|
+
ref: candidate.element.ref,
|
|
1662
|
+
role: candidate.element.role,
|
|
1663
|
+
name: candidate.element.name,
|
|
1664
|
+
disabled: candidate.element.disabled
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
usedTypes.add(hintType);
|
|
1668
|
+
} else if (hints.length < maxHints) {
|
|
1669
|
+
hints.push({
|
|
1670
|
+
selector: refSelector,
|
|
1671
|
+
reason: candidate.matchReason,
|
|
1672
|
+
confidence: getConfidence(candidate.score),
|
|
1673
|
+
element: {
|
|
1674
|
+
ref: candidate.element.ref,
|
|
1675
|
+
role: candidate.element.role,
|
|
1676
|
+
name: candidate.element.name,
|
|
1677
|
+
disabled: candidate.element.disabled
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return hints;
|
|
1683
|
+
}
|
|
1684
|
+
async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
1685
|
+
let snapshot;
|
|
1686
|
+
try {
|
|
1687
|
+
snapshot = await page.snapshot();
|
|
1688
|
+
} catch {
|
|
1689
|
+
return [];
|
|
1690
|
+
}
|
|
1691
|
+
const intent = extractIntent(failedSelectors);
|
|
1692
|
+
const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
|
|
1693
|
+
let candidates = snapshot.interactiveElements;
|
|
1694
|
+
if (roleFilter.length > 0) {
|
|
1695
|
+
candidates = candidates.filter((el) => roleFilter.includes(el.role));
|
|
1696
|
+
}
|
|
1697
|
+
const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
|
|
1698
|
+
if (matches.length === 0) {
|
|
1699
|
+
return [];
|
|
1700
|
+
}
|
|
1701
|
+
return diversifyHints(matches, maxHints);
|
|
1702
|
+
}
|
|
1440
1703
|
|
|
1441
1704
|
// src/browser/page.ts
|
|
1442
1705
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
1443
1706
|
var Page = class {
|
|
1444
1707
|
cdp;
|
|
1708
|
+
_targetId;
|
|
1445
1709
|
rootNodeId = null;
|
|
1446
1710
|
batchExecutor;
|
|
1447
1711
|
emulationState = {};
|
|
@@ -1460,10 +1724,19 @@ var Page = class {
|
|
|
1460
1724
|
frameExecutionContexts = /* @__PURE__ */ new Map();
|
|
1461
1725
|
/** Current frame's execution context ID (null = main frame default) */
|
|
1462
1726
|
currentFrameContextId = null;
|
|
1463
|
-
|
|
1727
|
+
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
1728
|
+
_lastMatchedSelector;
|
|
1729
|
+
constructor(cdp, targetId) {
|
|
1464
1730
|
this.cdp = cdp;
|
|
1731
|
+
this._targetId = targetId;
|
|
1465
1732
|
this.batchExecutor = new BatchExecutor(this);
|
|
1466
1733
|
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Get the CDP target ID for this page
|
|
1736
|
+
*/
|
|
1737
|
+
get targetId() {
|
|
1738
|
+
return this._targetId;
|
|
1739
|
+
}
|
|
1467
1740
|
/**
|
|
1468
1741
|
* Get the underlying CDP client for advanced operations.
|
|
1469
1742
|
* Use with caution - prefer high-level Page methods when possible.
|
|
@@ -1471,6 +1744,13 @@ var Page = class {
|
|
|
1471
1744
|
get cdpClient() {
|
|
1472
1745
|
return this.cdp;
|
|
1473
1746
|
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Get the last matched selector from findElement (for selectorUsed tracking).
|
|
1749
|
+
* Returns undefined if no selector has been matched yet.
|
|
1750
|
+
*/
|
|
1751
|
+
getLastMatchedSelector() {
|
|
1752
|
+
return this._lastMatchedSelector;
|
|
1753
|
+
}
|
|
1474
1754
|
/**
|
|
1475
1755
|
* Initialize the page (enable required CDP domains)
|
|
1476
1756
|
*/
|
|
@@ -1590,7 +1870,9 @@ var Page = class {
|
|
|
1590
1870
|
const element = await this.findElement(selector, options);
|
|
1591
1871
|
if (!element) {
|
|
1592
1872
|
if (options.optional) return false;
|
|
1593
|
-
|
|
1873
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1874
|
+
const hints = await generateHints(this, selectorList, "click");
|
|
1875
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1594
1876
|
}
|
|
1595
1877
|
await this.scrollIntoView(element.nodeId);
|
|
1596
1878
|
const submitResult = await this.evaluateInFrame(
|
|
@@ -1626,7 +1908,9 @@ var Page = class {
|
|
|
1626
1908
|
const element = await this.findElement(selector, options);
|
|
1627
1909
|
if (!element) {
|
|
1628
1910
|
if (options.optional) return false;
|
|
1629
|
-
|
|
1911
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1912
|
+
const hints = await generateHints(this, selectorList, "fill");
|
|
1913
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1630
1914
|
}
|
|
1631
1915
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
1632
1916
|
if (clear) {
|
|
@@ -1704,7 +1988,9 @@ var Page = class {
|
|
|
1704
1988
|
const element = await this.findElement(selector, options);
|
|
1705
1989
|
if (!element) {
|
|
1706
1990
|
if (options.optional) return false;
|
|
1707
|
-
|
|
1991
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1992
|
+
const hints = await generateHints(this, selectorList, "select");
|
|
1993
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1708
1994
|
}
|
|
1709
1995
|
const values = Array.isArray(value) ? value : [value];
|
|
1710
1996
|
await this.cdp.send("Runtime.evaluate", {
|
|
@@ -1765,7 +2051,9 @@ var Page = class {
|
|
|
1765
2051
|
const element = await this.findElement(selector, options);
|
|
1766
2052
|
if (!element) {
|
|
1767
2053
|
if (options.optional) return false;
|
|
1768
|
-
|
|
2054
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
2055
|
+
const hints = await generateHints(this, selectorList, "check");
|
|
2056
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1769
2057
|
}
|
|
1770
2058
|
const result = await this.cdp.send("Runtime.evaluate", {
|
|
1771
2059
|
expression: `(() => {
|
|
@@ -1785,7 +2073,9 @@ var Page = class {
|
|
|
1785
2073
|
const element = await this.findElement(selector, options);
|
|
1786
2074
|
if (!element) {
|
|
1787
2075
|
if (options.optional) return false;
|
|
1788
|
-
|
|
2076
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
2077
|
+
const hints = await generateHints(this, selectorList, "uncheck");
|
|
2078
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1789
2079
|
}
|
|
1790
2080
|
const result = await this.cdp.send("Runtime.evaluate", {
|
|
1791
2081
|
expression: `(() => {
|
|
@@ -1805,13 +2095,40 @@ var Page = class {
|
|
|
1805
2095
|
* - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
|
|
1806
2096
|
* - true: Wait for full navigation (traditional forms)
|
|
1807
2097
|
* - false: Return immediately (AJAX forms where you'll wait for something else)
|
|
2098
|
+
*
|
|
2099
|
+
* When targeting a <form> element directly, uses form.requestSubmit() which fires
|
|
2100
|
+
* the submit event and triggers HTML5 validation.
|
|
1808
2101
|
*/
|
|
1809
2102
|
async submit(selector, options = {}) {
|
|
1810
2103
|
const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
|
|
1811
2104
|
const element = await this.findElement(selector, options);
|
|
1812
2105
|
if (!element) {
|
|
1813
2106
|
if (options.optional) return false;
|
|
1814
|
-
|
|
2107
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
2108
|
+
const hints = await generateHints(this, selectorList, "submit");
|
|
2109
|
+
throw new ElementNotFoundError(selector, hints);
|
|
2110
|
+
}
|
|
2111
|
+
const isFormElement = await this.evaluateInFrame(
|
|
2112
|
+
`(() => {
|
|
2113
|
+
const el = document.querySelector(${JSON.stringify(element.selector)});
|
|
2114
|
+
return el instanceof HTMLFormElement;
|
|
2115
|
+
})()`
|
|
2116
|
+
);
|
|
2117
|
+
if (isFormElement.result.value) {
|
|
2118
|
+
await this.evaluateInFrame(
|
|
2119
|
+
`(() => {
|
|
2120
|
+
const form = document.querySelector(${JSON.stringify(element.selector)});
|
|
2121
|
+
if (form && form instanceof HTMLFormElement) {
|
|
2122
|
+
form.requestSubmit();
|
|
2123
|
+
}
|
|
2124
|
+
})()`
|
|
2125
|
+
);
|
|
2126
|
+
if (shouldWait === true) {
|
|
2127
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
2128
|
+
} else if (shouldWait === "auto") {
|
|
2129
|
+
await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep2(500)]);
|
|
2130
|
+
}
|
|
2131
|
+
return true;
|
|
1815
2132
|
}
|
|
1816
2133
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
1817
2134
|
if (method.includes("enter")) {
|
|
@@ -1882,7 +2199,9 @@ var Page = class {
|
|
|
1882
2199
|
const element = await this.findElement(selector, options);
|
|
1883
2200
|
if (!element) {
|
|
1884
2201
|
if (options.optional) return false;
|
|
1885
|
-
|
|
2202
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
2203
|
+
const hints = await generateHints(this, selectorList, "focus");
|
|
2204
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1886
2205
|
}
|
|
1887
2206
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
1888
2207
|
return true;
|
|
@@ -1895,7 +2214,9 @@ var Page = class {
|
|
|
1895
2214
|
const element = await this.findElement(selector, options);
|
|
1896
2215
|
if (!element) {
|
|
1897
2216
|
if (options.optional) return false;
|
|
1898
|
-
|
|
2217
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
2218
|
+
const hints = await generateHints(this, selectorList, "hover");
|
|
2219
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1899
2220
|
}
|
|
1900
2221
|
await this.scrollIntoView(element.nodeId);
|
|
1901
2222
|
const box = await this.getBoxModel(element.nodeId);
|
|
@@ -1966,7 +2287,7 @@ var Page = class {
|
|
|
1966
2287
|
const deadline = Date.now() + timeout;
|
|
1967
2288
|
let contextId = this.frameExecutionContexts.get(frameId);
|
|
1968
2289
|
while (!contextId && Date.now() < deadline) {
|
|
1969
|
-
await new Promise((
|
|
2290
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
1970
2291
|
contextId = this.frameExecutionContexts.get(frameId);
|
|
1971
2292
|
}
|
|
1972
2293
|
if (contextId) {
|
|
@@ -2145,7 +2466,7 @@ var Page = class {
|
|
|
2145
2466
|
behavior: "allowAndName",
|
|
2146
2467
|
eventsEnabled: true
|
|
2147
2468
|
});
|
|
2148
|
-
return new Promise((
|
|
2469
|
+
return new Promise((resolve2, reject) => {
|
|
2149
2470
|
let downloadGuid;
|
|
2150
2471
|
let suggestedFilename;
|
|
2151
2472
|
let resolved = false;
|
|
@@ -2169,7 +2490,7 @@ var Page = class {
|
|
|
2169
2490
|
return new ArrayBuffer(0);
|
|
2170
2491
|
}
|
|
2171
2492
|
};
|
|
2172
|
-
|
|
2493
|
+
resolve2(download);
|
|
2173
2494
|
} else if (params["guid"] === downloadGuid && params["state"] === "canceled") {
|
|
2174
2495
|
resolved = true;
|
|
2175
2496
|
cleanup();
|
|
@@ -2850,6 +3171,7 @@ var Page = class {
|
|
|
2850
3171
|
async findElement(selectors, options = {}) {
|
|
2851
3172
|
const { timeout = DEFAULT_TIMEOUT2 } = options;
|
|
2852
3173
|
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
3174
|
+
this._lastMatchedSelector = void 0;
|
|
2853
3175
|
for (const selector of selectorList) {
|
|
2854
3176
|
if (selector.startsWith("ref:")) {
|
|
2855
3177
|
const ref = selector.slice(4);
|
|
@@ -2866,6 +3188,7 @@ var Page = class {
|
|
|
2866
3188
|
}
|
|
2867
3189
|
);
|
|
2868
3190
|
if (pushResult.nodeIds?.[0]) {
|
|
3191
|
+
this._lastMatchedSelector = selector;
|
|
2869
3192
|
return {
|
|
2870
3193
|
nodeId: pushResult.nodeIds[0],
|
|
2871
3194
|
backendNodeId,
|
|
@@ -2899,6 +3222,7 @@ var Page = class {
|
|
|
2899
3222
|
"DOM.describeNode",
|
|
2900
3223
|
{ nodeId: queryResult.nodeId }
|
|
2901
3224
|
);
|
|
3225
|
+
this._lastMatchedSelector = result.selector;
|
|
2902
3226
|
return {
|
|
2903
3227
|
nodeId: queryResult.nodeId,
|
|
2904
3228
|
backendNodeId: describeResult2.node.backendNodeId,
|
|
@@ -2926,6 +3250,7 @@ var Page = class {
|
|
|
2926
3250
|
"DOM.describeNode",
|
|
2927
3251
|
{ nodeId: nodeResult.nodeId }
|
|
2928
3252
|
);
|
|
3253
|
+
this._lastMatchedSelector = result.selector;
|
|
2929
3254
|
return {
|
|
2930
3255
|
nodeId: nodeResult.nodeId,
|
|
2931
3256
|
backendNodeId: describeResult.node.backendNodeId,
|
|
@@ -3004,7 +3329,7 @@ var Page = class {
|
|
|
3004
3329
|
}
|
|
3005
3330
|
};
|
|
3006
3331
|
function sleep2(ms) {
|
|
3007
|
-
return new Promise((
|
|
3332
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3008
3333
|
}
|
|
3009
3334
|
|
|
3010
3335
|
// src/browser/browser.ts
|
|
@@ -3032,14 +3357,24 @@ var Browser = class _Browser {
|
|
|
3032
3357
|
* Get or create a page by name
|
|
3033
3358
|
* If no name is provided, returns the first available page or creates a new one
|
|
3034
3359
|
*/
|
|
3035
|
-
async page(name) {
|
|
3360
|
+
async page(name, options) {
|
|
3036
3361
|
const pageName = name ?? "default";
|
|
3037
3362
|
const cached = this.pages.get(pageName);
|
|
3038
3363
|
if (cached) return cached;
|
|
3039
3364
|
const targets = await this.cdp.send("Target.getTargets");
|
|
3040
3365
|
const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
3041
3366
|
let targetId;
|
|
3042
|
-
if (
|
|
3367
|
+
if (options?.targetId) {
|
|
3368
|
+
const targetExists = pageTargets.some((t) => t.targetId === options.targetId);
|
|
3369
|
+
if (targetExists) {
|
|
3370
|
+
targetId = options.targetId;
|
|
3371
|
+
} else {
|
|
3372
|
+
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
3373
|
+
targetId = pageTargets.length > 0 ? pageTargets[0].targetId : (await this.cdp.send("Target.createTarget", {
|
|
3374
|
+
url: "about:blank"
|
|
3375
|
+
})).targetId;
|
|
3376
|
+
}
|
|
3377
|
+
} else if (pageTargets.length > 0) {
|
|
3043
3378
|
targetId = pageTargets[0].targetId;
|
|
3044
3379
|
} else {
|
|
3045
3380
|
const result = await this.cdp.send("Target.createTarget", {
|
|
@@ -3048,7 +3383,7 @@ var Browser = class _Browser {
|
|
|
3048
3383
|
targetId = result.targetId;
|
|
3049
3384
|
}
|
|
3050
3385
|
await this.cdp.attachToTarget(targetId);
|
|
3051
|
-
const page = new Page(this.cdp);
|
|
3386
|
+
const page = new Page(this.cdp, targetId);
|
|
3052
3387
|
await page.init();
|
|
3053
3388
|
this.pages.set(pageName, page);
|
|
3054
3389
|
return page;
|
|
@@ -3061,7 +3396,7 @@ var Browser = class _Browser {
|
|
|
3061
3396
|
url
|
|
3062
3397
|
});
|
|
3063
3398
|
await this.cdp.attachToTarget(result.targetId);
|
|
3064
|
-
const page = new Page(this.cdp);
|
|
3399
|
+
const page = new Page(this.cdp, result.targetId);
|
|
3065
3400
|
await page.init();
|
|
3066
3401
|
const name = `page-${this.pages.size + 1}`;
|
|
3067
3402
|
this.pages.set(name, page);
|
|
@@ -3182,6 +3517,8 @@ function parseConnectArgs(args) {
|
|
|
3182
3517
|
options.apiKey = args[++i];
|
|
3183
3518
|
} else if (arg === "--project-id") {
|
|
3184
3519
|
options.projectId = args[++i];
|
|
3520
|
+
} else if (arg === "--export-log") {
|
|
3521
|
+
options.exportLog = args[++i];
|
|
3185
3522
|
}
|
|
3186
3523
|
}
|
|
3187
3524
|
return options;
|
|
@@ -3230,6 +3567,8 @@ async function connectCommand(args, globalOptions) {
|
|
|
3230
3567
|
provider,
|
|
3231
3568
|
wsUrl: browser.wsUrl,
|
|
3232
3569
|
providerSessionId: browser.sessionId,
|
|
3570
|
+
targetId: page.targetId,
|
|
3571
|
+
exportLog: options.exportLog,
|
|
3233
3572
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3234
3573
|
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3235
3574
|
currentUrl,
|
|
@@ -3249,6 +3588,808 @@ async function connectCommand(args, globalOptions) {
|
|
|
3249
3588
|
);
|
|
3250
3589
|
}
|
|
3251
3590
|
|
|
3591
|
+
// src/browser/selector-generator.ts
|
|
3592
|
+
function escapeAttrValue(value) {
|
|
3593
|
+
return value.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
3594
|
+
}
|
|
3595
|
+
function extractTestId(selector) {
|
|
3596
|
+
const patterns = [
|
|
3597
|
+
/\[data-testid=["']([^"']+)["']\]/,
|
|
3598
|
+
/\[data-test-id=["']([^"']+)["']\]/,
|
|
3599
|
+
/\[data-test=["']([^"']+)["']\]/
|
|
3600
|
+
];
|
|
3601
|
+
for (const pattern of patterns) {
|
|
3602
|
+
const match = selector.match(pattern);
|
|
3603
|
+
if (match) {
|
|
3604
|
+
return match[1] ?? null;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return null;
|
|
3608
|
+
}
|
|
3609
|
+
function extractId(selector) {
|
|
3610
|
+
const match = selector.match(/#([a-zA-Z][a-zA-Z0-9_-]*)/);
|
|
3611
|
+
return match ? match[1] ?? null : null;
|
|
3612
|
+
}
|
|
3613
|
+
function generateRoleSelector(element) {
|
|
3614
|
+
if (!element.role || !element.name) {
|
|
3615
|
+
return null;
|
|
3616
|
+
}
|
|
3617
|
+
const escapedName = escapeAttrValue(element.name);
|
|
3618
|
+
return `[role="${element.role}"][aria-label="${escapedName}"]`;
|
|
3619
|
+
}
|
|
3620
|
+
function simplifyCssPath(selector) {
|
|
3621
|
+
if (selector.startsWith("#") || selector.startsWith(".") || selector.startsWith("[")) {
|
|
3622
|
+
return selector;
|
|
3623
|
+
}
|
|
3624
|
+
const parts = selector.split(/\s+/);
|
|
3625
|
+
const lastPart = parts[parts.length - 1];
|
|
3626
|
+
if (lastPart && (lastPart.startsWith("#") || lastPart.includes("[") || lastPart.includes("."))) {
|
|
3627
|
+
return lastPart;
|
|
3628
|
+
}
|
|
3629
|
+
return selector;
|
|
3630
|
+
}
|
|
3631
|
+
function generateSelectors(element, _snapshot) {
|
|
3632
|
+
const selectors = [];
|
|
3633
|
+
if (element.ref) {
|
|
3634
|
+
selectors.push({
|
|
3635
|
+
selector: `ref:${element.ref}`,
|
|
3636
|
+
type: "ref"
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
const testId = extractTestId(element.selector);
|
|
3640
|
+
if (testId) {
|
|
3641
|
+
selectors.push({
|
|
3642
|
+
selector: `[data-testid="${escapeAttrValue(testId)}"]`,
|
|
3643
|
+
type: "testid"
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
if (element.name) {
|
|
3647
|
+
selectors.push({
|
|
3648
|
+
selector: `[aria-label="${escapeAttrValue(element.name)}"]`,
|
|
3649
|
+
type: "aria-label"
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
const id = extractId(element.selector);
|
|
3653
|
+
if (id) {
|
|
3654
|
+
selectors.push({
|
|
3655
|
+
selector: `#${id}`,
|
|
3656
|
+
type: "id"
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
const roleSelector = generateRoleSelector(element);
|
|
3660
|
+
if (roleSelector) {
|
|
3661
|
+
selectors.push({
|
|
3662
|
+
selector: roleSelector,
|
|
3663
|
+
type: "role-name"
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
const cssPath = simplifyCssPath(element.selector);
|
|
3667
|
+
if (cssPath && !selectors.some((s) => s.selector === cssPath)) {
|
|
3668
|
+
selectors.push({
|
|
3669
|
+
selector: cssPath,
|
|
3670
|
+
type: "css"
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
return selectors;
|
|
3674
|
+
}
|
|
3675
|
+
function generateSelectorStrings(element, snapshot) {
|
|
3676
|
+
return generateSelectors(element, snapshot).map((s) => s.selector);
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/browser/visibility.ts
|
|
3680
|
+
var VISIBILITY_STATE_SCRIPT = `
|
|
3681
|
+
function getVisibilityState(selector) {
|
|
3682
|
+
${DEEP_QUERY_SCRIPT}
|
|
3683
|
+
const el = deepQuery(selector);
|
|
3684
|
+
if (!el) {
|
|
3685
|
+
return null;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
const style = getComputedStyle(el);
|
|
3689
|
+
const rect = el.getBoundingClientRect();
|
|
3690
|
+
|
|
3691
|
+
const display = style.display;
|
|
3692
|
+
const visibility = style.visibility;
|
|
3693
|
+
const opacity = parseFloat(style.opacity);
|
|
3694
|
+
const width = rect.width;
|
|
3695
|
+
const height = rect.height;
|
|
3696
|
+
|
|
3697
|
+
// Check if in viewport
|
|
3698
|
+
const inViewport = (
|
|
3699
|
+
rect.top < window.innerHeight &&
|
|
3700
|
+
rect.bottom > 0 &&
|
|
3701
|
+
rect.left < window.innerWidth &&
|
|
3702
|
+
rect.right > 0
|
|
3703
|
+
);
|
|
3704
|
+
|
|
3705
|
+
// Collect reasons for invisibility
|
|
3706
|
+
const reasons = [];
|
|
3707
|
+
if (display === 'none') {
|
|
3708
|
+
reasons.push('display: none');
|
|
3709
|
+
}
|
|
3710
|
+
if (visibility === 'hidden') {
|
|
3711
|
+
reasons.push('visibility: hidden');
|
|
3712
|
+
}
|
|
3713
|
+
if (opacity === 0) {
|
|
3714
|
+
reasons.push('opacity: 0');
|
|
3715
|
+
}
|
|
3716
|
+
if (width === 0 && height === 0) {
|
|
3717
|
+
reasons.push('zero dimensions');
|
|
3718
|
+
} else if (width === 0) {
|
|
3719
|
+
reasons.push('zero width');
|
|
3720
|
+
} else if (height === 0) {
|
|
3721
|
+
reasons.push('zero height');
|
|
3722
|
+
}
|
|
3723
|
+
if (!inViewport && width > 0 && height > 0) {
|
|
3724
|
+
reasons.push('outside viewport');
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
const visible = reasons.length === 0;
|
|
3728
|
+
|
|
3729
|
+
return {
|
|
3730
|
+
visible,
|
|
3731
|
+
display,
|
|
3732
|
+
visibility,
|
|
3733
|
+
opacity,
|
|
3734
|
+
width,
|
|
3735
|
+
height,
|
|
3736
|
+
inViewport,
|
|
3737
|
+
reasons
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
`;
|
|
3741
|
+
var COVERING_ELEMENT_SCRIPT = `
|
|
3742
|
+
function detectCoveringElement(selector) {
|
|
3743
|
+
${DEEP_QUERY_SCRIPT}
|
|
3744
|
+
const el = deepQuery(selector);
|
|
3745
|
+
if (!el) {
|
|
3746
|
+
return { error: 'Element not found' };
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
const rect = el.getBoundingClientRect();
|
|
3750
|
+
|
|
3751
|
+
// Check center point
|
|
3752
|
+
const centerX = rect.left + rect.width / 2;
|
|
3753
|
+
const centerY = rect.top + rect.height / 2;
|
|
3754
|
+
|
|
3755
|
+
// Get element at center point
|
|
3756
|
+
const topEl = document.elementFromPoint(centerX, centerY);
|
|
3757
|
+
|
|
3758
|
+
if (!topEl) {
|
|
3759
|
+
return { covered: false };
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// Check if the target element is the top element or contains it
|
|
3763
|
+
if (topEl === el || el.contains(topEl)) {
|
|
3764
|
+
return { covered: false };
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// Element is covered - get info about covering element
|
|
3768
|
+
const style = getComputedStyle(topEl);
|
|
3769
|
+
return {
|
|
3770
|
+
covered: true,
|
|
3771
|
+
coveringElement: {
|
|
3772
|
+
tagName: topEl.tagName.toLowerCase(),
|
|
3773
|
+
id: topEl.id || undefined,
|
|
3774
|
+
className: topEl.className || undefined,
|
|
3775
|
+
zIndex: style.zIndex === 'auto' ? undefined : parseInt(style.zIndex, 10)
|
|
3776
|
+
}
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
`;
|
|
3780
|
+
var VISIBILITY_BY_NODE_SCRIPT = `
|
|
3781
|
+
function getVisibilityStateByNode(nodeId) {
|
|
3782
|
+
// This script expects nodeId to be resolved to an element via CDP
|
|
3783
|
+
// It's called after Runtime.callFunctionOn with the target element
|
|
3784
|
+
const el = this;
|
|
3785
|
+
if (!el) {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
const style = getComputedStyle(el);
|
|
3790
|
+
const rect = el.getBoundingClientRect();
|
|
3791
|
+
|
|
3792
|
+
const display = style.display;
|
|
3793
|
+
const visibility = style.visibility;
|
|
3794
|
+
const opacity = parseFloat(style.opacity);
|
|
3795
|
+
const width = rect.width;
|
|
3796
|
+
const height = rect.height;
|
|
3797
|
+
|
|
3798
|
+
const inViewport = (
|
|
3799
|
+
rect.top < window.innerHeight &&
|
|
3800
|
+
rect.bottom > 0 &&
|
|
3801
|
+
rect.left < window.innerWidth &&
|
|
3802
|
+
rect.right > 0
|
|
3803
|
+
);
|
|
3804
|
+
|
|
3805
|
+
const reasons = [];
|
|
3806
|
+
if (display === 'none') reasons.push('display: none');
|
|
3807
|
+
if (visibility === 'hidden') reasons.push('visibility: hidden');
|
|
3808
|
+
if (opacity === 0) reasons.push('opacity: 0');
|
|
3809
|
+
if (width === 0 && height === 0) reasons.push('zero dimensions');
|
|
3810
|
+
else if (width === 0) reasons.push('zero width');
|
|
3811
|
+
else if (height === 0) reasons.push('zero height');
|
|
3812
|
+
if (!inViewport && width > 0 && height > 0) reasons.push('outside viewport');
|
|
3813
|
+
|
|
3814
|
+
return {
|
|
3815
|
+
visible: reasons.length === 0,
|
|
3816
|
+
display,
|
|
3817
|
+
visibility,
|
|
3818
|
+
opacity,
|
|
3819
|
+
width,
|
|
3820
|
+
height,
|
|
3821
|
+
inViewport,
|
|
3822
|
+
reasons
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
`;
|
|
3826
|
+
var COVERING_BY_NODE_SCRIPT = `
|
|
3827
|
+
function detectCoveringByNode() {
|
|
3828
|
+
const el = this;
|
|
3829
|
+
if (!el) {
|
|
3830
|
+
return { error: 'Element not found' };
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
const rect = el.getBoundingClientRect();
|
|
3834
|
+
const centerX = rect.left + rect.width / 2;
|
|
3835
|
+
const centerY = rect.top + rect.height / 2;
|
|
3836
|
+
|
|
3837
|
+
const topEl = document.elementFromPoint(centerX, centerY);
|
|
3838
|
+
|
|
3839
|
+
if (!topEl) {
|
|
3840
|
+
return { covered: false };
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
if (topEl === el || el.contains(topEl)) {
|
|
3844
|
+
return { covered: false };
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
const style = getComputedStyle(topEl);
|
|
3848
|
+
return {
|
|
3849
|
+
covered: true,
|
|
3850
|
+
coveringElement: {
|
|
3851
|
+
tagName: topEl.tagName.toLowerCase(),
|
|
3852
|
+
id: topEl.id || undefined,
|
|
3853
|
+
className: topEl.className || undefined,
|
|
3854
|
+
zIndex: style.zIndex === 'auto' ? undefined : parseInt(style.zIndex, 10)
|
|
3855
|
+
}
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
`;
|
|
3859
|
+
async function getVisibilityState(cdp, nodeId) {
|
|
3860
|
+
const resolveResult = await cdp.send("DOM.resolveNode", {
|
|
3861
|
+
nodeId
|
|
3862
|
+
});
|
|
3863
|
+
if (!resolveResult.object?.objectId) {
|
|
3864
|
+
return null;
|
|
3865
|
+
}
|
|
3866
|
+
const objectId = resolveResult.object.objectId;
|
|
3867
|
+
const result = await cdp.send(
|
|
3868
|
+
"Runtime.callFunctionOn",
|
|
3869
|
+
{
|
|
3870
|
+
objectId,
|
|
3871
|
+
functionDeclaration: VISIBILITY_BY_NODE_SCRIPT,
|
|
3872
|
+
returnByValue: true
|
|
3873
|
+
}
|
|
3874
|
+
);
|
|
3875
|
+
return result.result.value;
|
|
3876
|
+
}
|
|
3877
|
+
async function detectCoveringElement(cdp, nodeId) {
|
|
3878
|
+
const resolveResult = await cdp.send("DOM.resolveNode", {
|
|
3879
|
+
nodeId
|
|
3880
|
+
});
|
|
3881
|
+
if (!resolveResult.object?.objectId) {
|
|
3882
|
+
return null;
|
|
3883
|
+
}
|
|
3884
|
+
const objectId = resolveResult.object.objectId;
|
|
3885
|
+
const result = await cdp.send("Runtime.callFunctionOn", {
|
|
3886
|
+
objectId,
|
|
3887
|
+
functionDeclaration: COVERING_BY_NODE_SCRIPT,
|
|
3888
|
+
returnByValue: true
|
|
3889
|
+
});
|
|
3890
|
+
const value = result.result.value;
|
|
3891
|
+
if (value.error || !value.covered) {
|
|
3892
|
+
return null;
|
|
3893
|
+
}
|
|
3894
|
+
return value.coveringElement ?? null;
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
// src/browser/diagnose.ts
|
|
3898
|
+
function isFuzzyQuery(selector) {
|
|
3899
|
+
if (selector.startsWith("ref:")) return false;
|
|
3900
|
+
if (/^[#.[]/.test(selector)) return false;
|
|
3901
|
+
if (/\[.*\]/.test(selector)) return false;
|
|
3902
|
+
if (/\s/.test(selector) && !/\s*[>+~]\s*/.test(selector)) return true;
|
|
3903
|
+
if (!/[#.[\]>+~:=]/.test(selector)) return true;
|
|
3904
|
+
return false;
|
|
3905
|
+
}
|
|
3906
|
+
async function tryExactMatch(page, selector) {
|
|
3907
|
+
const cdp = page.cdpClient;
|
|
3908
|
+
if (selector.startsWith("ref:")) {
|
|
3909
|
+
const ref = selector.slice(4);
|
|
3910
|
+
const refMap = page.exportRefMap();
|
|
3911
|
+
const backendNodeId = refMap[ref];
|
|
3912
|
+
if (!backendNodeId) {
|
|
3913
|
+
return { found: false };
|
|
3914
|
+
}
|
|
3915
|
+
try {
|
|
3916
|
+
await cdp.send("DOM.getDocument");
|
|
3917
|
+
const pushResult = await cdp.send(
|
|
3918
|
+
"DOM.pushNodesByBackendIdsToFrontend",
|
|
3919
|
+
{
|
|
3920
|
+
backendNodeIds: [backendNodeId]
|
|
3921
|
+
}
|
|
3922
|
+
);
|
|
3923
|
+
if (pushResult.nodeIds?.[0]) {
|
|
3924
|
+
return {
|
|
3925
|
+
found: true,
|
|
3926
|
+
nodeId: pushResult.nodeIds[0],
|
|
3927
|
+
backendNodeId,
|
|
3928
|
+
selectorUsed: selector
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
} catch {
|
|
3932
|
+
return { found: false };
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
try {
|
|
3936
|
+
const doc = await cdp.send("DOM.getDocument");
|
|
3937
|
+
const rootNodeId = doc.root.nodeId;
|
|
3938
|
+
const result = await cdp.send("DOM.querySelector", {
|
|
3939
|
+
nodeId: rootNodeId,
|
|
3940
|
+
selector
|
|
3941
|
+
});
|
|
3942
|
+
if (result.nodeId && result.nodeId !== 0) {
|
|
3943
|
+
const describe = await cdp.send("DOM.describeNode", {
|
|
3944
|
+
nodeId: result.nodeId
|
|
3945
|
+
});
|
|
3946
|
+
return {
|
|
3947
|
+
found: true,
|
|
3948
|
+
nodeId: result.nodeId,
|
|
3949
|
+
backendNodeId: describe.node.backendNodeId,
|
|
3950
|
+
selectorUsed: selector
|
|
3951
|
+
};
|
|
3952
|
+
}
|
|
3953
|
+
} catch {
|
|
3954
|
+
}
|
|
3955
|
+
return { found: false };
|
|
3956
|
+
}
|
|
3957
|
+
async function getElementAttributes(page, nodeId) {
|
|
3958
|
+
const cdp = page.cdpClient;
|
|
3959
|
+
const attributes = {};
|
|
3960
|
+
try {
|
|
3961
|
+
const result = await cdp.send("DOM.getAttributes", {
|
|
3962
|
+
nodeId
|
|
3963
|
+
});
|
|
3964
|
+
for (let i = 0; i < result.attributes.length; i += 2) {
|
|
3965
|
+
const name = result.attributes[i];
|
|
3966
|
+
const value = result.attributes[i + 1];
|
|
3967
|
+
if (name && value !== void 0) {
|
|
3968
|
+
attributes[name] = value;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
} catch {
|
|
3972
|
+
}
|
|
3973
|
+
return attributes;
|
|
3974
|
+
}
|
|
3975
|
+
async function diagnoseElement(page, selector, options = {}) {
|
|
3976
|
+
const { maxCandidates = 5, includeHidden = false } = options;
|
|
3977
|
+
const cdp = page.cdpClient;
|
|
3978
|
+
const snapshot = await page.snapshot();
|
|
3979
|
+
const fuzzy = isFuzzyQuery(selector);
|
|
3980
|
+
if (!fuzzy) {
|
|
3981
|
+
const match = await tryExactMatch(page, selector);
|
|
3982
|
+
if (match.found && match.nodeId && match.backendNodeId) {
|
|
3983
|
+
const visibility = await getVisibilityState(cdp, match.nodeId) ?? {
|
|
3984
|
+
visible: false,
|
|
3985
|
+
display: "unknown",
|
|
3986
|
+
visibility: "unknown",
|
|
3987
|
+
opacity: 1,
|
|
3988
|
+
width: 0,
|
|
3989
|
+
height: 0,
|
|
3990
|
+
inViewport: false,
|
|
3991
|
+
reasons: ["Could not determine visibility"]
|
|
3992
|
+
};
|
|
3993
|
+
const covering = await detectCoveringElement(cdp, match.nodeId);
|
|
3994
|
+
const attributes = await getElementAttributes(page, match.nodeId);
|
|
3995
|
+
const refMap = page.exportRefMap();
|
|
3996
|
+
let ref = "";
|
|
3997
|
+
for (const [r, bid] of Object.entries(refMap)) {
|
|
3998
|
+
if (bid === match.backendNodeId) {
|
|
3999
|
+
ref = r;
|
|
4000
|
+
break;
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
const interactiveEl = snapshot.interactiveElements.find((el) => el.ref === ref);
|
|
4004
|
+
const disabled = attributes["disabled"] !== void 0 || interactiveEl?.disabled === true;
|
|
4005
|
+
const readonly = attributes["readonly"] !== void 0;
|
|
4006
|
+
const covered = covering !== null;
|
|
4007
|
+
const clickable = visibility.visible && !disabled && !covered;
|
|
4008
|
+
let reason;
|
|
4009
|
+
if (!clickable) {
|
|
4010
|
+
const reasons = [];
|
|
4011
|
+
if (!visibility.visible) reasons.push("not visible");
|
|
4012
|
+
if (disabled) reasons.push("disabled");
|
|
4013
|
+
if (covered) reasons.push("covered by another element");
|
|
4014
|
+
reason = reasons.join(", ");
|
|
4015
|
+
}
|
|
4016
|
+
const element = interactiveEl ?? {
|
|
4017
|
+
ref,
|
|
4018
|
+
role: "generic",
|
|
4019
|
+
name: "",
|
|
4020
|
+
selector: match.selectorUsed ?? selector,
|
|
4021
|
+
disabled
|
|
4022
|
+
};
|
|
4023
|
+
const suggestedSelectors = generateSelectorStrings(element, snapshot);
|
|
4024
|
+
return {
|
|
4025
|
+
matched: true,
|
|
4026
|
+
selector: match.selectorUsed ?? selector,
|
|
4027
|
+
ref,
|
|
4028
|
+
element: {
|
|
4029
|
+
role: element.role,
|
|
4030
|
+
name: element.name,
|
|
4031
|
+
nodeId: match.nodeId,
|
|
4032
|
+
backendNodeId: match.backendNodeId
|
|
4033
|
+
},
|
|
4034
|
+
visibility,
|
|
4035
|
+
interactivity: {
|
|
4036
|
+
disabled,
|
|
4037
|
+
readonly,
|
|
4038
|
+
covered,
|
|
4039
|
+
coveringElement: covering ?? void 0,
|
|
4040
|
+
clickable,
|
|
4041
|
+
reason
|
|
4042
|
+
},
|
|
4043
|
+
attributes,
|
|
4044
|
+
suggestedSelectors
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
let candidates = snapshot.interactiveElements;
|
|
4049
|
+
if (!includeHidden) {
|
|
4050
|
+
candidates = candidates.filter((el) => !el.disabled);
|
|
4051
|
+
}
|
|
4052
|
+
const matches = fuzzyMatchElements(selector, candidates, maxCandidates);
|
|
4053
|
+
return {
|
|
4054
|
+
matched: false,
|
|
4055
|
+
query: selector,
|
|
4056
|
+
candidates: matches.map((m) => ({
|
|
4057
|
+
score: m.score,
|
|
4058
|
+
ref: m.element.ref,
|
|
4059
|
+
selector: m.element.selector,
|
|
4060
|
+
role: m.element.role,
|
|
4061
|
+
name: m.element.name,
|
|
4062
|
+
visible: true,
|
|
4063
|
+
// We assume visible since they're interactive
|
|
4064
|
+
disabled: m.element.disabled ?? false,
|
|
4065
|
+
matchReason: m.matchReason
|
|
4066
|
+
}))
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/cli/commands/diagnose.ts
|
|
4071
|
+
var DIAGNOSE_HELP = `
|
|
4072
|
+
bp diagnose - Debug element selection and find alternatives
|
|
4073
|
+
|
|
4074
|
+
Usage:
|
|
4075
|
+
bp diagnose <selector> Diagnose specific selector
|
|
4076
|
+
bp diagnose "<fuzzy query>" Fuzzy search for elements
|
|
4077
|
+
|
|
4078
|
+
Examples:
|
|
4079
|
+
bp diagnose "#login-btn" Full diagnostics for element
|
|
4080
|
+
bp diagnose "submit" Find elements matching "submit"
|
|
4081
|
+
bp diagnose "ref:e4" Diagnose by element ref
|
|
4082
|
+
|
|
4083
|
+
Options:
|
|
4084
|
+
--json Output as JSON
|
|
4085
|
+
--max <n> Max candidates for fuzzy match (default: 5)
|
|
4086
|
+
-s, --session <id> Use specific session
|
|
4087
|
+
--help Show this help
|
|
4088
|
+
|
|
4089
|
+
Output (exact match):
|
|
4090
|
+
- Visibility: display, opacity, in viewport
|
|
4091
|
+
- Interactivity: disabled, covered by overlay
|
|
4092
|
+
- Alternative selectors
|
|
4093
|
+
|
|
4094
|
+
Output (fuzzy match):
|
|
4095
|
+
- Top N candidates ranked by similarity
|
|
4096
|
+
- Role, name, visibility for each
|
|
4097
|
+
`;
|
|
4098
|
+
function parseDiagnoseArgs(args) {
|
|
4099
|
+
const options = {};
|
|
4100
|
+
let selector;
|
|
4101
|
+
for (let i = 0; i < args.length; i++) {
|
|
4102
|
+
const arg = args[i];
|
|
4103
|
+
if (arg === "--max") {
|
|
4104
|
+
options.maxCandidates = parseInt(args[++i] ?? "5", 10);
|
|
4105
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
4106
|
+
options.help = true;
|
|
4107
|
+
} else if (!arg.startsWith("-")) {
|
|
4108
|
+
selector = arg;
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
return { selector, options };
|
|
4112
|
+
}
|
|
4113
|
+
function formatExactResult(result) {
|
|
4114
|
+
const lines = [];
|
|
4115
|
+
lines.push(`\u2713 Element Found: ${result.selector}`);
|
|
4116
|
+
lines.push(` Ref: ${result.ref}`);
|
|
4117
|
+
lines.push(` Role: ${result.element.role}`);
|
|
4118
|
+
if (result.element.name) {
|
|
4119
|
+
lines.push(` Name: "${result.element.name}"`);
|
|
4120
|
+
}
|
|
4121
|
+
lines.push("");
|
|
4122
|
+
lines.push("Visibility:");
|
|
4123
|
+
lines.push(` Visible: ${result.visibility.visible ? "\u2713 Yes" : "\u2717 No"}`);
|
|
4124
|
+
if (!result.visibility.visible && result.visibility.reasons.length > 0) {
|
|
4125
|
+
lines.push(` Reasons: ${result.visibility.reasons.join(", ")}`);
|
|
4126
|
+
}
|
|
4127
|
+
lines.push(` Display: ${result.visibility.display}`);
|
|
4128
|
+
lines.push(` Opacity: ${result.visibility.opacity}`);
|
|
4129
|
+
lines.push(` Size: ${result.visibility.width}x${result.visibility.height}`);
|
|
4130
|
+
lines.push(` In Viewport: ${result.visibility.inViewport ? "Yes" : "No"}`);
|
|
4131
|
+
lines.push("");
|
|
4132
|
+
lines.push("Interactivity:");
|
|
4133
|
+
lines.push(` Clickable: ${result.interactivity.clickable ? "\u2713 Yes" : "\u2717 No"}`);
|
|
4134
|
+
if (!result.interactivity.clickable && result.interactivity.reason) {
|
|
4135
|
+
lines.push(` Reason: ${result.interactivity.reason}`);
|
|
4136
|
+
}
|
|
4137
|
+
lines.push(` Disabled: ${result.interactivity.disabled ? "Yes" : "No"}`);
|
|
4138
|
+
lines.push(` Readonly: ${result.interactivity.readonly ? "Yes" : "No"}`);
|
|
4139
|
+
lines.push(` Covered: ${result.interactivity.covered ? "Yes" : "No"}`);
|
|
4140
|
+
if (result.interactivity.coveringElement) {
|
|
4141
|
+
const ce = result.interactivity.coveringElement;
|
|
4142
|
+
lines.push(` Covering Element: <${ce.tagName}${ce.id ? ` id="${ce.id}"` : ""}>`);
|
|
4143
|
+
}
|
|
4144
|
+
lines.push("");
|
|
4145
|
+
if (result.suggestedSelectors.length > 0) {
|
|
4146
|
+
lines.push("Alternative Selectors:");
|
|
4147
|
+
for (const sel of result.suggestedSelectors) {
|
|
4148
|
+
lines.push(` - ${sel}`);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
return lines.join("\n");
|
|
4152
|
+
}
|
|
4153
|
+
function formatFuzzyResult(result) {
|
|
4154
|
+
const lines = [];
|
|
4155
|
+
lines.push(`\u2717 No exact match for: "${result.query}"`);
|
|
4156
|
+
lines.push("");
|
|
4157
|
+
if (result.candidates.length === 0) {
|
|
4158
|
+
lines.push("No similar elements found.");
|
|
4159
|
+
return lines.join("\n");
|
|
4160
|
+
}
|
|
4161
|
+
lines.push(`Found ${result.candidates.length} similar elements:`);
|
|
4162
|
+
lines.push("");
|
|
4163
|
+
for (let i = 0; i < result.candidates.length; i++) {
|
|
4164
|
+
const c = result.candidates[i];
|
|
4165
|
+
const score = (c.score * 100).toFixed(0);
|
|
4166
|
+
lines.push(`${i + 1}. [${c.ref}] ${c.role} "${c.name || "(no name)"}" (${score}% match)`);
|
|
4167
|
+
lines.push(` Selector: ${c.selector}`);
|
|
4168
|
+
lines.push(` Reason: ${c.matchReason}`);
|
|
4169
|
+
if (c.disabled) {
|
|
4170
|
+
lines.push(` \u26A0 Disabled`);
|
|
4171
|
+
}
|
|
4172
|
+
lines.push("");
|
|
4173
|
+
}
|
|
4174
|
+
return lines.join("\n");
|
|
4175
|
+
}
|
|
4176
|
+
async function diagnoseCommand(args, globalOptions) {
|
|
4177
|
+
const { selector, options } = parseDiagnoseArgs(args);
|
|
4178
|
+
if (options.help || !selector) {
|
|
4179
|
+
console.log(DIAGNOSE_HELP);
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
let session;
|
|
4183
|
+
if (globalOptions.session) {
|
|
4184
|
+
session = await loadSession(globalOptions.session);
|
|
4185
|
+
} else {
|
|
4186
|
+
session = await getDefaultSession();
|
|
4187
|
+
if (!session) {
|
|
4188
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
const browser = await connect({
|
|
4192
|
+
provider: session.provider,
|
|
4193
|
+
wsUrl: session.wsUrl,
|
|
4194
|
+
debug: globalOptions.trace
|
|
4195
|
+
});
|
|
4196
|
+
try {
|
|
4197
|
+
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
4198
|
+
const result = await diagnoseElement(page, selector, {
|
|
4199
|
+
maxCandidates: options.maxCandidates
|
|
4200
|
+
});
|
|
4201
|
+
const snapshot = await page.snapshot();
|
|
4202
|
+
await updateSession(session.id, {
|
|
4203
|
+
currentUrl: snapshot.url,
|
|
4204
|
+
metadata: {
|
|
4205
|
+
refCache: {
|
|
4206
|
+
url: snapshot.url,
|
|
4207
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4208
|
+
refMap: page.exportRefMap()
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
});
|
|
4212
|
+
if (globalOptions.output === "json") {
|
|
4213
|
+
output(result, "json");
|
|
4214
|
+
} else {
|
|
4215
|
+
if (result.matched) {
|
|
4216
|
+
console.log(formatExactResult(result));
|
|
4217
|
+
} else {
|
|
4218
|
+
console.log(formatFuzzyResult(result));
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
} finally {
|
|
4222
|
+
await browser.disconnect();
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
// src/cli/session-logger.ts
|
|
4227
|
+
var fs = __toESM(require("fs"), 1);
|
|
4228
|
+
var import_node_os2 = require("os");
|
|
4229
|
+
var import_node_path2 = require("path");
|
|
4230
|
+
var SESSION_DIR2 = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".browser-pilot", "sessions");
|
|
4231
|
+
var SessionLogger = class {
|
|
4232
|
+
logPath;
|
|
4233
|
+
exportLogPath = null;
|
|
4234
|
+
seq = 0;
|
|
4235
|
+
constructor(sessionId, exportLogPath) {
|
|
4236
|
+
const sessionDir = (0, import_node_path2.join)(SESSION_DIR2, sessionId);
|
|
4237
|
+
this.logPath = (0, import_node_path2.join)(sessionDir, "log.jsonl");
|
|
4238
|
+
if (!fs.existsSync(sessionDir)) {
|
|
4239
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
4240
|
+
}
|
|
4241
|
+
if (exportLogPath) {
|
|
4242
|
+
this.exportLogPath = (0, import_node_path2.resolve)(exportLogPath);
|
|
4243
|
+
const exportDir = (0, import_node_path2.dirname)(this.exportLogPath);
|
|
4244
|
+
if (!fs.existsSync(exportDir)) {
|
|
4245
|
+
fs.mkdirSync(exportDir, { recursive: true });
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
if (fs.existsSync(this.logPath)) {
|
|
4249
|
+
this.seq = this.countEntries();
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* Log a raw entry (writes to both core and export logs)
|
|
4254
|
+
*/
|
|
4255
|
+
log(entry) {
|
|
4256
|
+
const fullEntry = {
|
|
4257
|
+
seq: ++this.seq,
|
|
4258
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4259
|
+
...entry
|
|
4260
|
+
};
|
|
4261
|
+
const line = `${JSON.stringify(fullEntry)}
|
|
4262
|
+
`;
|
|
4263
|
+
fs.appendFileSync(this.logPath, line, "utf-8");
|
|
4264
|
+
if (this.exportLogPath) {
|
|
4265
|
+
try {
|
|
4266
|
+
fs.appendFileSync(this.exportLogPath, line, "utf-8");
|
|
4267
|
+
} catch (err) {
|
|
4268
|
+
console.warn(`[browser-pilot] Failed to write to export log: ${err}`);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
/**
|
|
4273
|
+
* Get the export log path (if configured)
|
|
4274
|
+
*/
|
|
4275
|
+
getExportLogPath() {
|
|
4276
|
+
return this.exportLogPath;
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* Log a command execution
|
|
4280
|
+
*/
|
|
4281
|
+
logCommand(cmd, args, result, durationMs) {
|
|
4282
|
+
this.log({
|
|
4283
|
+
type: "command",
|
|
4284
|
+
cmd,
|
|
4285
|
+
args,
|
|
4286
|
+
status: result.success ? "success" : "failed",
|
|
4287
|
+
durationMs,
|
|
4288
|
+
error: result.error,
|
|
4289
|
+
hints: result.hints
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Log an error
|
|
4294
|
+
*/
|
|
4295
|
+
logError(error, context) {
|
|
4296
|
+
this.log({
|
|
4297
|
+
type: "error",
|
|
4298
|
+
error: error.message,
|
|
4299
|
+
args: context
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
/**
|
|
4303
|
+
* Get the log file path
|
|
4304
|
+
*/
|
|
4305
|
+
getLogPath() {
|
|
4306
|
+
return this.logPath;
|
|
4307
|
+
}
|
|
4308
|
+
/**
|
|
4309
|
+
* Get log statistics
|
|
4310
|
+
*/
|
|
4311
|
+
getLogStats() {
|
|
4312
|
+
if (!fs.existsSync(this.logPath)) {
|
|
4313
|
+
return { entries: 0, size: 0 };
|
|
4314
|
+
}
|
|
4315
|
+
const stat = fs.statSync(this.logPath);
|
|
4316
|
+
const entries = this.countEntries();
|
|
4317
|
+
let first;
|
|
4318
|
+
let last;
|
|
4319
|
+
if (entries > 0) {
|
|
4320
|
+
const lines = fs.readFileSync(this.logPath, "utf-8").trim().split("\n");
|
|
4321
|
+
const firstEntry = this.parseLine(lines[0]);
|
|
4322
|
+
const lastEntry = this.parseLine(lines[lines.length - 1]);
|
|
4323
|
+
first = firstEntry?.ts;
|
|
4324
|
+
last = lastEntry?.ts;
|
|
4325
|
+
}
|
|
4326
|
+
return {
|
|
4327
|
+
entries,
|
|
4328
|
+
size: stat.size,
|
|
4329
|
+
first,
|
|
4330
|
+
last
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Get the last n log entries
|
|
4335
|
+
*/
|
|
4336
|
+
tailLog(n) {
|
|
4337
|
+
if (!fs.existsSync(this.logPath)) {
|
|
4338
|
+
return [];
|
|
4339
|
+
}
|
|
4340
|
+
const content = fs.readFileSync(this.logPath, "utf-8").trim();
|
|
4341
|
+
if (!content) {
|
|
4342
|
+
return [];
|
|
4343
|
+
}
|
|
4344
|
+
const lines = content.split("\n");
|
|
4345
|
+
const startIndex = Math.max(0, lines.length - n);
|
|
4346
|
+
const result = [];
|
|
4347
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
4348
|
+
const entry = this.parseLine(lines[i]);
|
|
4349
|
+
if (entry) {
|
|
4350
|
+
result.push(entry);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
return result;
|
|
4354
|
+
}
|
|
4355
|
+
/**
|
|
4356
|
+
* Count entries in the log file
|
|
4357
|
+
*/
|
|
4358
|
+
countEntries() {
|
|
4359
|
+
if (!fs.existsSync(this.logPath)) {
|
|
4360
|
+
return 0;
|
|
4361
|
+
}
|
|
4362
|
+
const content = fs.readFileSync(this.logPath, "utf-8").trim();
|
|
4363
|
+
if (!content) {
|
|
4364
|
+
return 0;
|
|
4365
|
+
}
|
|
4366
|
+
return content.split("\n").length;
|
|
4367
|
+
}
|
|
4368
|
+
/**
|
|
4369
|
+
* Parse a single log line
|
|
4370
|
+
*/
|
|
4371
|
+
parseLine(line) {
|
|
4372
|
+
if (!line) {
|
|
4373
|
+
return null;
|
|
4374
|
+
}
|
|
4375
|
+
try {
|
|
4376
|
+
return JSON.parse(line);
|
|
4377
|
+
} catch {
|
|
4378
|
+
return null;
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4382
|
+
var loggerCache = /* @__PURE__ */ new Map();
|
|
4383
|
+
function getSessionLogger(sessionId, exportLogPath) {
|
|
4384
|
+
const cacheKey = exportLogPath ? `${sessionId}:${exportLogPath}` : sessionId;
|
|
4385
|
+
let logger = loggerCache.get(cacheKey);
|
|
4386
|
+
if (!logger) {
|
|
4387
|
+
logger = new SessionLogger(sessionId, exportLogPath);
|
|
4388
|
+
loggerCache.set(cacheKey, logger);
|
|
4389
|
+
}
|
|
4390
|
+
return logger;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
3252
4393
|
// src/cli/commands/exec.ts
|
|
3253
4394
|
async function validateSession(session) {
|
|
3254
4395
|
try {
|
|
@@ -3313,13 +4454,14 @@ Run 'bp actions' for complete action reference.`
|
|
|
3313
4454
|
Session file has been cleaned up. Run "bp connect" to create a new session.`
|
|
3314
4455
|
);
|
|
3315
4456
|
}
|
|
4457
|
+
const logger = getSessionLogger(session.id, session.exportLog);
|
|
3316
4458
|
const browser = await connect({
|
|
3317
4459
|
provider: session.provider,
|
|
3318
4460
|
wsUrl: session.wsUrl,
|
|
3319
4461
|
debug: globalOptions.trace
|
|
3320
4462
|
});
|
|
3321
4463
|
try {
|
|
3322
|
-
const page = addBatchToPage(await browser.page());
|
|
4464
|
+
const page = addBatchToPage(await browser.page(void 0, { targetId: session.targetId }));
|
|
3323
4465
|
const currentUrlForCache = await page.url();
|
|
3324
4466
|
const refCache = session.metadata?.refCache;
|
|
3325
4467
|
if (refCache && refCache.url === currentUrlForCache) {
|
|
@@ -3335,7 +4477,30 @@ Session file has been cleaned up. Run "bp connect" to create a new session.`
|
|
|
3335
4477
|
});
|
|
3336
4478
|
}
|
|
3337
4479
|
const steps = Array.isArray(actions) ? actions : [actions];
|
|
4480
|
+
const urlBefore = await page.url();
|
|
3338
4481
|
const result = await page.batch(steps);
|
|
4482
|
+
const urlAfter = await page.url();
|
|
4483
|
+
for (const stepResult of result.steps) {
|
|
4484
|
+
logger.logCommand(
|
|
4485
|
+
stepResult.action,
|
|
4486
|
+
{ selector: stepResult.selectorUsed },
|
|
4487
|
+
{
|
|
4488
|
+
success: stepResult.success,
|
|
4489
|
+
error: stepResult.error,
|
|
4490
|
+
hints: stepResult.hints
|
|
4491
|
+
},
|
|
4492
|
+
stepResult.durationMs
|
|
4493
|
+
);
|
|
4494
|
+
}
|
|
4495
|
+
logger.log({
|
|
4496
|
+
type: "event",
|
|
4497
|
+
cmd: "batch",
|
|
4498
|
+
args: { stepCount: steps.length },
|
|
4499
|
+
status: result.success ? "success" : "failed",
|
|
4500
|
+
durationMs: result.totalDurationMs,
|
|
4501
|
+
urlBefore,
|
|
4502
|
+
urlAfter
|
|
4503
|
+
});
|
|
3339
4504
|
const currentUrl = await page.url();
|
|
3340
4505
|
const hasSnapshot = steps.some((step) => step.action === "snapshot");
|
|
3341
4506
|
if (hasSnapshot) {
|
|
@@ -3376,7 +4541,101 @@ Session file has been cleaned up. Run "bp connect" to create a new session.`
|
|
|
3376
4541
|
}
|
|
3377
4542
|
|
|
3378
4543
|
// src/cli/commands/list.ts
|
|
3379
|
-
|
|
4544
|
+
function parseListArgs(args) {
|
|
4545
|
+
const options = {};
|
|
4546
|
+
for (let i = 0; i < args.length; i++) {
|
|
4547
|
+
const arg = args[i];
|
|
4548
|
+
if (arg === "--log-path") {
|
|
4549
|
+
options.logPath = true;
|
|
4550
|
+
} else if (arg === "--log-tail") {
|
|
4551
|
+
const next = args[i + 1];
|
|
4552
|
+
if (next && !next.startsWith("-")) {
|
|
4553
|
+
options.logTail = parseInt(next, 10);
|
|
4554
|
+
i++;
|
|
4555
|
+
} else {
|
|
4556
|
+
options.logTail = 20;
|
|
4557
|
+
}
|
|
4558
|
+
} else if (arg === "--info") {
|
|
4559
|
+
options.info = true;
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
return options;
|
|
4563
|
+
}
|
|
4564
|
+
function formatLogEntry(entry) {
|
|
4565
|
+
const time = new Date(entry.ts).toLocaleTimeString();
|
|
4566
|
+
const status = entry.status === "failed" ? "\u2717" : entry.status === "success" ? "\u2713" : "\u25CB";
|
|
4567
|
+
if (entry.type === "command") {
|
|
4568
|
+
const dur = entry.durationMs ? `(${entry.durationMs}ms)` : "";
|
|
4569
|
+
return `${time} ${status} ${entry.cmd} ${dur}`;
|
|
4570
|
+
}
|
|
4571
|
+
if (entry.type === "error") {
|
|
4572
|
+
return `${time} \u2717 ERROR: ${entry.error}`;
|
|
4573
|
+
}
|
|
4574
|
+
return `${time} ${entry.type}: ${entry.cmd || ""}`;
|
|
4575
|
+
}
|
|
4576
|
+
async function listCommand(args, globalOptions) {
|
|
4577
|
+
const listOptions = parseListArgs(args);
|
|
4578
|
+
if (listOptions.logPath || listOptions.logTail !== void 0 || listOptions.info) {
|
|
4579
|
+
let session;
|
|
4580
|
+
if (globalOptions.session) {
|
|
4581
|
+
session = await loadSession(globalOptions.session);
|
|
4582
|
+
} else {
|
|
4583
|
+
session = await getDefaultSession();
|
|
4584
|
+
}
|
|
4585
|
+
if (!session) {
|
|
4586
|
+
throw new Error('No session found. Run "bp connect" first or specify with -s.');
|
|
4587
|
+
}
|
|
4588
|
+
const logger = getSessionLogger(session.id);
|
|
4589
|
+
if (listOptions.logPath) {
|
|
4590
|
+
console.log(logger.getLogPath());
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
if (listOptions.logTail !== void 0) {
|
|
4594
|
+
const entries = logger.tailLog(listOptions.logTail);
|
|
4595
|
+
if (globalOptions.output === "json") {
|
|
4596
|
+
output(entries, "json");
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
if (entries.length === 0) {
|
|
4600
|
+
console.log("No log entries.");
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
console.log(`Last ${entries.length} log entries for session ${session.id}:
|
|
4604
|
+
`);
|
|
4605
|
+
for (const entry of entries) {
|
|
4606
|
+
console.log(` ${formatLogEntry(entry)}`);
|
|
4607
|
+
}
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
if (listOptions.info) {
|
|
4611
|
+
const stats = logger.getLogStats();
|
|
4612
|
+
if (globalOptions.output === "json") {
|
|
4613
|
+
output({ session, logStats: stats }, "json");
|
|
4614
|
+
return;
|
|
4615
|
+
}
|
|
4616
|
+
console.log(`Session: ${session.id}
|
|
4617
|
+
`);
|
|
4618
|
+
console.log(` Provider: ${session.provider}`);
|
|
4619
|
+
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
4620
|
+
console.log(` Last activity: ${new Date(session.lastActivity).toLocaleString()}`);
|
|
4621
|
+
console.log(` URL: ${session.currentUrl}`);
|
|
4622
|
+
if (session.exportLog) {
|
|
4623
|
+
console.log(` Export log: ${session.exportLog}`);
|
|
4624
|
+
}
|
|
4625
|
+
console.log("");
|
|
4626
|
+
console.log("Log Stats:");
|
|
4627
|
+
console.log(` Path: ${logger.getLogPath()}`);
|
|
4628
|
+
console.log(` Entries: ${stats.entries}`);
|
|
4629
|
+
console.log(` Size: ${formatBytes(stats.size)}`);
|
|
4630
|
+
if (stats.first) {
|
|
4631
|
+
console.log(` First: ${new Date(stats.first).toLocaleString()}`);
|
|
4632
|
+
}
|
|
4633
|
+
if (stats.last) {
|
|
4634
|
+
console.log(` Last: ${new Date(stats.last).toLocaleString()}`);
|
|
4635
|
+
}
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
3380
4639
|
const sessions = await listSessions();
|
|
3381
4640
|
if (globalOptions.output === "json") {
|
|
3382
4641
|
output(sessions, "json");
|
|
@@ -3397,6 +4656,11 @@ async function listCommand(_args, globalOptions) {
|
|
|
3397
4656
|
console.log("");
|
|
3398
4657
|
}
|
|
3399
4658
|
}
|
|
4659
|
+
function formatBytes(bytes) {
|
|
4660
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4661
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4662
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
4663
|
+
}
|
|
3400
4664
|
function getAge(date) {
|
|
3401
4665
|
const now = Date.now();
|
|
3402
4666
|
const diff = now - date.getTime();
|
|
@@ -3442,9 +4706,9 @@ STEP 5: BATCH MULTIPLE ACTIONS
|
|
|
3442
4706
|
]'
|
|
3443
4707
|
|
|
3444
4708
|
FOR AI AGENTS
|
|
3445
|
-
Use
|
|
3446
|
-
bp snapshot --format text
|
|
3447
|
-
bp exec '{"action":"click","selector":"ref:e3"}'
|
|
4709
|
+
Use --json for machine-readable output:
|
|
4710
|
+
bp snapshot --format text --json
|
|
4711
|
+
bp exec '{"action":"click","selector":"ref:e3"}' --json
|
|
3448
4712
|
|
|
3449
4713
|
TIPS
|
|
3450
4714
|
\u2022 Refs (e1, e2...) are stable within a page - prefer them over CSS selectors
|
|
@@ -3476,6 +4740,31 @@ RECORDING (FOR HUMANS)
|
|
|
3476
4740
|
|
|
3477
4741
|
Great for creating initial automation scripts that AI agents can refine.
|
|
3478
4742
|
|
|
4743
|
+
DEBUGGING
|
|
4744
|
+
When element selection fails, use these tools to diagnose:
|
|
4745
|
+
|
|
4746
|
+
1. Diagnose a selector:
|
|
4747
|
+
bp diagnose '#submit-button' -s mysite
|
|
4748
|
+
|
|
4749
|
+
Shows exact matches, fuzzy matches, visibility issues, and suggestions.
|
|
4750
|
+
|
|
4751
|
+
2. Compare page states:
|
|
4752
|
+
bp snapshot > before.json
|
|
4753
|
+
# ... perform actions ...
|
|
4754
|
+
bp snapshot --diff before.json
|
|
4755
|
+
|
|
4756
|
+
Shows what changed: added/removed/modified elements.
|
|
4757
|
+
|
|
4758
|
+
3. Visual inspection:
|
|
4759
|
+
bp snapshot --inspect
|
|
4760
|
+
|
|
4761
|
+
Injects visual ref labels onto the page. Use --keep to leave them visible.
|
|
4762
|
+
|
|
4763
|
+
4. Session logs:
|
|
4764
|
+
bp list -s mysite --log-tail 10
|
|
4765
|
+
|
|
4766
|
+
Shows last N commands with timing and any errors.
|
|
4767
|
+
|
|
3479
4768
|
Run 'bp actions' for the complete action reference.
|
|
3480
4769
|
`;
|
|
3481
4770
|
async function quickstartCommand() {
|
|
@@ -4433,8 +5722,8 @@ async function recordCommand(args, globalOptions) {
|
|
|
4433
5722
|
stopping = true;
|
|
4434
5723
|
try {
|
|
4435
5724
|
const recording = await recorder.stop();
|
|
4436
|
-
const
|
|
4437
|
-
await
|
|
5725
|
+
const fs3 = await import("fs/promises");
|
|
5726
|
+
await fs3.writeFile(outputFile, JSON.stringify(recording, null, 2));
|
|
4438
5727
|
const currentUrl = await page.url();
|
|
4439
5728
|
await updateSession(session.id, { currentUrl });
|
|
4440
5729
|
await browser.disconnect();
|
|
@@ -4502,7 +5791,7 @@ async function screenshotCommand(args, globalOptions) {
|
|
|
4502
5791
|
debug: globalOptions.trace
|
|
4503
5792
|
});
|
|
4504
5793
|
try {
|
|
4505
|
-
const page = await browser.page();
|
|
5794
|
+
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
4506
5795
|
const screenshotData = await page.screenshot({
|
|
4507
5796
|
format: options.format ?? "png",
|
|
4508
5797
|
quality: options.quality,
|
|
@@ -4532,6 +5821,256 @@ async function screenshotCommand(args, globalOptions) {
|
|
|
4532
5821
|
}
|
|
4533
5822
|
}
|
|
4534
5823
|
|
|
5824
|
+
// src/cli/commands/snapshot.ts
|
|
5825
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
5826
|
+
|
|
5827
|
+
// src/browser/overlay.ts
|
|
5828
|
+
var OVERLAY_SCRIPT = `(function() {
|
|
5829
|
+
// Check for existing DOM elements (handles cross-CLI reconnection)
|
|
5830
|
+
let style = document.getElementById('__bp-overlay-styles');
|
|
5831
|
+
let container = document.getElementById('__bp-overlay-container');
|
|
5832
|
+
|
|
5833
|
+
// Clear existing labels before updating
|
|
5834
|
+
if (container) {
|
|
5835
|
+
container.innerHTML = '';
|
|
5836
|
+
}
|
|
5837
|
+
document.querySelectorAll('[data-bp-ref]').forEach(el => el.removeAttribute('data-bp-ref'));
|
|
5838
|
+
|
|
5839
|
+
// Create infrastructure only if it doesn't exist
|
|
5840
|
+
if (!style) {
|
|
5841
|
+
style = document.createElement('style');
|
|
5842
|
+
style.id = '__bp-overlay-styles';
|
|
5843
|
+
style.textContent = \`
|
|
5844
|
+
[data-bp-ref] {
|
|
5845
|
+
outline: 2px dashed rgba(229, 57, 53, 0.6) !important;
|
|
5846
|
+
outline-offset: 2px !important;
|
|
5847
|
+
}
|
|
5848
|
+
.__bp-ref-label {
|
|
5849
|
+
position: absolute;
|
|
5850
|
+
background: #e53935;
|
|
5851
|
+
color: white;
|
|
5852
|
+
padding: 1px 4px;
|
|
5853
|
+
font-size: 10px;
|
|
5854
|
+
font-family: monospace;
|
|
5855
|
+
font-weight: bold;
|
|
5856
|
+
z-index: 10000;
|
|
5857
|
+
pointer-events: none;
|
|
5858
|
+
border-radius: 2px;
|
|
5859
|
+
line-height: 1.2;
|
|
5860
|
+
}
|
|
5861
|
+
\`;
|
|
5862
|
+
document.head.appendChild(style);
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
if (!container) {
|
|
5866
|
+
container = document.createElement('div');
|
|
5867
|
+
container.id = '__bp-overlay-container';
|
|
5868
|
+
container.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10000;';
|
|
5869
|
+
document.body.appendChild(container);
|
|
5870
|
+
}
|
|
5871
|
+
|
|
5872
|
+
// Always redefine to ensure correct container reference
|
|
5873
|
+
window.__bpAddLabel = function(ref, rect) {
|
|
5874
|
+
const label = document.createElement('div');
|
|
5875
|
+
label.className = '__bp-ref-label';
|
|
5876
|
+
label.textContent = ref;
|
|
5877
|
+
label.style.left = (rect.left + window.scrollX) + 'px';
|
|
5878
|
+
label.style.top = (rect.top + window.scrollY - 16) + 'px';
|
|
5879
|
+
container.appendChild(label);
|
|
5880
|
+
};
|
|
5881
|
+
|
|
5882
|
+
window.__bpRemoveOverlay = function() {
|
|
5883
|
+
const c = document.getElementById('__bp-overlay-container');
|
|
5884
|
+
const s = document.getElementById('__bp-overlay-styles');
|
|
5885
|
+
if (c) c.remove();
|
|
5886
|
+
if (s) s.remove();
|
|
5887
|
+
document.querySelectorAll('[data-bp-ref]').forEach(el => el.removeAttribute('data-bp-ref'));
|
|
5888
|
+
delete window.__bpOverlayInstalled;
|
|
5889
|
+
delete window.__bpAddLabel;
|
|
5890
|
+
delete window.__bpRemoveOverlay;
|
|
5891
|
+
};
|
|
5892
|
+
|
|
5893
|
+
window.__bpOverlayInstalled = true;
|
|
5894
|
+
})();`;
|
|
5895
|
+
var REMOVE_OVERLAY_SCRIPT = `(function() {
|
|
5896
|
+
if (window.__bpRemoveOverlay) {
|
|
5897
|
+
window.__bpRemoveOverlay();
|
|
5898
|
+
}
|
|
5899
|
+
})();`;
|
|
5900
|
+
var ADD_REF_SCRIPT = `function(ref) {
|
|
5901
|
+
this.setAttribute('data-bp-ref', ref);
|
|
5902
|
+
const rect = this.getBoundingClientRect();
|
|
5903
|
+
if (window.__bpAddLabel) {
|
|
5904
|
+
window.__bpAddLabel(ref, {
|
|
5905
|
+
left: rect.left,
|
|
5906
|
+
top: rect.top,
|
|
5907
|
+
width: rect.width,
|
|
5908
|
+
height: rect.height
|
|
5909
|
+
});
|
|
5910
|
+
}
|
|
5911
|
+
return true;
|
|
5912
|
+
}`;
|
|
5913
|
+
async function injectRefOverlay(page, snapshot) {
|
|
5914
|
+
await page.evaluate(OVERLAY_SCRIPT);
|
|
5915
|
+
const refMap = page.exportRefMap();
|
|
5916
|
+
const cdp = page.cdpClient;
|
|
5917
|
+
for (const element of snapshot.interactiveElements) {
|
|
5918
|
+
const backendNodeId = refMap[element.ref];
|
|
5919
|
+
if (backendNodeId === void 0) {
|
|
5920
|
+
continue;
|
|
5921
|
+
}
|
|
5922
|
+
try {
|
|
5923
|
+
const resolveResult = await cdp.send("DOM.resolveNode", {
|
|
5924
|
+
backendNodeId
|
|
5925
|
+
});
|
|
5926
|
+
if (!resolveResult.object?.objectId) {
|
|
5927
|
+
continue;
|
|
5928
|
+
}
|
|
5929
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
5930
|
+
objectId: resolveResult.object.objectId,
|
|
5931
|
+
functionDeclaration: ADD_REF_SCRIPT,
|
|
5932
|
+
arguments: [{ value: element.ref }],
|
|
5933
|
+
returnByValue: true
|
|
5934
|
+
});
|
|
5935
|
+
} catch {
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
async function removeRefOverlay(page) {
|
|
5940
|
+
await page.evaluate(REMOVE_OVERLAY_SCRIPT);
|
|
5941
|
+
}
|
|
5942
|
+
|
|
5943
|
+
// src/browser/snapshot-diff.ts
|
|
5944
|
+
function getElementKey(node, path) {
|
|
5945
|
+
const name = node.name ?? "";
|
|
5946
|
+
return `${node.role}::${name}::${path.join("/")}`;
|
|
5947
|
+
}
|
|
5948
|
+
function flattenTree(nodes, path = []) {
|
|
5949
|
+
const result = [];
|
|
5950
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
5951
|
+
const node = nodes[i];
|
|
5952
|
+
const currentPath = [...path, String(i)];
|
|
5953
|
+
const key = getElementKey(node, currentPath);
|
|
5954
|
+
result.push({ node, key, path: currentPath });
|
|
5955
|
+
if (node.children) {
|
|
5956
|
+
result.push(...flattenTree(node.children, currentPath));
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
return result;
|
|
5960
|
+
}
|
|
5961
|
+
function compareNodes(before, after) {
|
|
5962
|
+
const changedFields = [];
|
|
5963
|
+
if (before.role !== after.role) {
|
|
5964
|
+
changedFields.push("role");
|
|
5965
|
+
}
|
|
5966
|
+
if (before.name !== after.name) {
|
|
5967
|
+
changedFields.push("name");
|
|
5968
|
+
}
|
|
5969
|
+
if (before.value !== after.value) {
|
|
5970
|
+
changedFields.push("value");
|
|
5971
|
+
}
|
|
5972
|
+
if (before.disabled !== after.disabled) {
|
|
5973
|
+
changedFields.push("disabled");
|
|
5974
|
+
}
|
|
5975
|
+
if (before.checked !== after.checked) {
|
|
5976
|
+
changedFields.push("checked");
|
|
5977
|
+
}
|
|
5978
|
+
return changedFields;
|
|
5979
|
+
}
|
|
5980
|
+
function diffSnapshots(before, after) {
|
|
5981
|
+
const beforeFlat = flattenTree(before.accessibilityTree);
|
|
5982
|
+
const afterFlat = flattenTree(after.accessibilityTree);
|
|
5983
|
+
const beforeMap = new Map(beforeFlat.map((item) => [item.key, item]));
|
|
5984
|
+
const afterMap = new Map(afterFlat.map((item) => [item.key, item]));
|
|
5985
|
+
const added = [];
|
|
5986
|
+
const removed = [];
|
|
5987
|
+
const changed = [];
|
|
5988
|
+
let unchanged = 0;
|
|
5989
|
+
for (const afterItem of afterFlat) {
|
|
5990
|
+
const beforeItem = beforeMap.get(afterItem.key);
|
|
5991
|
+
if (!beforeItem) {
|
|
5992
|
+
added.push(afterItem.node);
|
|
5993
|
+
} else {
|
|
5994
|
+
const changedFields = compareNodes(beforeItem.node, afterItem.node);
|
|
5995
|
+
if (changedFields.length > 0) {
|
|
5996
|
+
changed.push({
|
|
5997
|
+
key: afterItem.key,
|
|
5998
|
+
before: beforeItem.node,
|
|
5999
|
+
after: afterItem.node,
|
|
6000
|
+
changedFields
|
|
6001
|
+
});
|
|
6002
|
+
} else {
|
|
6003
|
+
unchanged++;
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
for (const beforeItem of beforeFlat) {
|
|
6008
|
+
if (!afterMap.has(beforeItem.key)) {
|
|
6009
|
+
removed.push(beforeItem.node);
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
return {
|
|
6013
|
+
metadata: {
|
|
6014
|
+
before: {
|
|
6015
|
+
url: before.url,
|
|
6016
|
+
timestamp: before.timestamp,
|
|
6017
|
+
title: before.title
|
|
6018
|
+
},
|
|
6019
|
+
after: {
|
|
6020
|
+
url: after.url,
|
|
6021
|
+
timestamp: after.timestamp,
|
|
6022
|
+
title: after.title
|
|
6023
|
+
},
|
|
6024
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6025
|
+
},
|
|
6026
|
+
summary: {
|
|
6027
|
+
added: added.length,
|
|
6028
|
+
removed: removed.length,
|
|
6029
|
+
changed: changed.length,
|
|
6030
|
+
unchanged
|
|
6031
|
+
},
|
|
6032
|
+
changes: {
|
|
6033
|
+
added,
|
|
6034
|
+
removed,
|
|
6035
|
+
changed
|
|
6036
|
+
}
|
|
6037
|
+
};
|
|
6038
|
+
}
|
|
6039
|
+
function formatDiffPretty(diff) {
|
|
6040
|
+
const lines = [];
|
|
6041
|
+
lines.push(`Snapshot Diff: ${diff.metadata.after.url}`);
|
|
6042
|
+
lines.push(` Before: ${diff.metadata.before.timestamp}`);
|
|
6043
|
+
lines.push(` After: ${diff.metadata.after.timestamp}`);
|
|
6044
|
+
lines.push("");
|
|
6045
|
+
if (diff.summary.added === 0 && diff.summary.removed === 0 && diff.summary.changed === 0) {
|
|
6046
|
+
lines.push("No changes detected.");
|
|
6047
|
+
return lines.join("\n");
|
|
6048
|
+
}
|
|
6049
|
+
lines.push("Changes:");
|
|
6050
|
+
for (const node of diff.changes.added) {
|
|
6051
|
+
const name = node.name ? ` "${node.name}"` : "";
|
|
6052
|
+
lines.push(` + [${node.ref}] ${node.role}${name} (new)`);
|
|
6053
|
+
}
|
|
6054
|
+
for (const item of diff.changes.changed) {
|
|
6055
|
+
const name = item.after.name ? ` "${item.after.name}"` : "";
|
|
6056
|
+
const fieldChanges = item.changedFields.map((field) => {
|
|
6057
|
+
const beforeVal = item.before[field];
|
|
6058
|
+
const afterVal = item.after[field];
|
|
6059
|
+
return `${field}: ${JSON.stringify(beforeVal)} \u2192 ${JSON.stringify(afterVal)}`;
|
|
6060
|
+
}).join(", ");
|
|
6061
|
+
lines.push(` ~ [${item.after.ref}] ${item.after.role}${name} ${fieldChanges}`);
|
|
6062
|
+
}
|
|
6063
|
+
for (const node of diff.changes.removed) {
|
|
6064
|
+
const name = node.name ? ` "${node.name}"` : "";
|
|
6065
|
+
lines.push(` - [${node.ref}] ${node.role}${name} (removed)`);
|
|
6066
|
+
}
|
|
6067
|
+
lines.push("");
|
|
6068
|
+
lines.push(
|
|
6069
|
+
`Summary: +${diff.summary.added} added, -${diff.summary.removed} removed, ~${diff.summary.changed} changed`
|
|
6070
|
+
);
|
|
6071
|
+
return lines.join("\n");
|
|
6072
|
+
}
|
|
6073
|
+
|
|
4535
6074
|
// src/cli/commands/snapshot.ts
|
|
4536
6075
|
function parseSnapshotArgs(args) {
|
|
4537
6076
|
const options = {};
|
|
@@ -4539,10 +6078,19 @@ function parseSnapshotArgs(args) {
|
|
|
4539
6078
|
const arg = args[i];
|
|
4540
6079
|
if (arg === "--format" || arg === "-f") {
|
|
4541
6080
|
options.format = args[++i];
|
|
6081
|
+
} else if (arg === "--diff" || arg === "-d") {
|
|
6082
|
+
options.diffFile = args[++i];
|
|
6083
|
+
} else if (arg === "--inspect") {
|
|
6084
|
+
options.inspect = true;
|
|
6085
|
+
} else if (arg === "--keep") {
|
|
6086
|
+
options.keep = true;
|
|
4542
6087
|
}
|
|
4543
6088
|
}
|
|
4544
6089
|
return options;
|
|
4545
6090
|
}
|
|
6091
|
+
function sleep3(ms) {
|
|
6092
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6093
|
+
}
|
|
4546
6094
|
async function snapshotCommand(args, globalOptions) {
|
|
4547
6095
|
const options = parseSnapshotArgs(args);
|
|
4548
6096
|
let session;
|
|
@@ -4560,7 +6108,7 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
4560
6108
|
debug: globalOptions.trace
|
|
4561
6109
|
});
|
|
4562
6110
|
try {
|
|
4563
|
-
const page = await browser.page();
|
|
6111
|
+
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
4564
6112
|
const snapshot = await page.snapshot();
|
|
4565
6113
|
await updateSession(session.id, {
|
|
4566
6114
|
currentUrl: snapshot.url,
|
|
@@ -4572,6 +6120,34 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
4572
6120
|
}
|
|
4573
6121
|
}
|
|
4574
6122
|
});
|
|
6123
|
+
if (options.diffFile) {
|
|
6124
|
+
if (!fs2.existsSync(options.diffFile)) {
|
|
6125
|
+
throw new Error(`Diff file not found: ${options.diffFile}`);
|
|
6126
|
+
}
|
|
6127
|
+
const beforeContent = fs2.readFileSync(options.diffFile, "utf-8");
|
|
6128
|
+
const beforeSnapshot = JSON.parse(beforeContent);
|
|
6129
|
+
const diff = diffSnapshots(beforeSnapshot, snapshot);
|
|
6130
|
+
if (globalOptions.output === "json") {
|
|
6131
|
+
output(diff, "json");
|
|
6132
|
+
} else {
|
|
6133
|
+
console.log(formatDiffPretty(diff));
|
|
6134
|
+
}
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
if (options.inspect) {
|
|
6138
|
+
await injectRefOverlay(page, snapshot);
|
|
6139
|
+
console.log("Overlay injected. Element refs are now visible on the page.");
|
|
6140
|
+
if (options.keep) {
|
|
6141
|
+
console.log(
|
|
6142
|
+
"Overlay will remain visible. Use removeRefOverlay() or refresh the page to remove."
|
|
6143
|
+
);
|
|
6144
|
+
} else {
|
|
6145
|
+
console.log("Overlay will be removed in 10 seconds...");
|
|
6146
|
+
await sleep3(1e4);
|
|
6147
|
+
await removeRefOverlay(page);
|
|
6148
|
+
console.log("Overlay removed.");
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
4575
6151
|
switch (options.format) {
|
|
4576
6152
|
case "interactive":
|
|
4577
6153
|
output(snapshot.interactiveElements, globalOptions.output);
|
|
@@ -4616,7 +6192,7 @@ async function textCommand(args, globalOptions) {
|
|
|
4616
6192
|
debug: globalOptions.trace
|
|
4617
6193
|
});
|
|
4618
6194
|
try {
|
|
4619
|
-
const page = await browser.page();
|
|
6195
|
+
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
4620
6196
|
const text = await page.text(options.selector);
|
|
4621
6197
|
const currentUrl = await page.url();
|
|
4622
6198
|
await updateSession(session.id, { currentUrl });
|
|
@@ -4643,27 +6219,29 @@ Commands:
|
|
|
4643
6219
|
exec Execute actions
|
|
4644
6220
|
record Record browser actions to JSON
|
|
4645
6221
|
snapshot Get page with element refs
|
|
6222
|
+
diagnose Debug element selection issues
|
|
4646
6223
|
text Extract text content
|
|
4647
6224
|
screenshot Take screenshot
|
|
4648
6225
|
close Close session
|
|
4649
|
-
list List sessions
|
|
6226
|
+
list List sessions (--log-path, --log-tail, --info)
|
|
4650
6227
|
clean Clean up old sessions
|
|
4651
6228
|
actions Complete action reference
|
|
4652
6229
|
|
|
4653
6230
|
Options:
|
|
4654
6231
|
-s, --session <id> Session ID
|
|
4655
6232
|
-o, --output <fmt> json | pretty (default: pretty)
|
|
6233
|
+
--json Alias for -o json
|
|
4656
6234
|
--trace Enable debug tracing
|
|
4657
6235
|
--dialog <mode> Handle dialogs: accept | dismiss
|
|
4658
6236
|
-h, --help Show help
|
|
4659
6237
|
|
|
4660
6238
|
Examples:
|
|
4661
6239
|
bp connect --provider generic --name dev
|
|
6240
|
+
bp connect -s test --export-log ./logs/test.jsonl
|
|
4662
6241
|
bp exec '{"action":"goto","url":"https://example.com"}'
|
|
4663
6242
|
bp snapshot --format text
|
|
6243
|
+
bp list --json # JSON output
|
|
4664
6244
|
bp exec '{"action":"click","selector":"ref:e3"}'
|
|
4665
|
-
bp record # Record from local browser
|
|
4666
|
-
bp record -s -f login.json # Record from latest session
|
|
4667
6245
|
|
|
4668
6246
|
Run 'bp quickstart' for CLI workflow guide.
|
|
4669
6247
|
Run 'bp actions' for complete action reference.
|
|
@@ -4679,6 +6257,10 @@ function parseGlobalOptions(args) {
|
|
|
4679
6257
|
options.session = args[++i];
|
|
4680
6258
|
} else if (arg === "-o" || arg === "--output") {
|
|
4681
6259
|
options.output = args[++i];
|
|
6260
|
+
} else if (arg === "--json") {
|
|
6261
|
+
options.output = "json";
|
|
6262
|
+
} else if (arg === "--pretty") {
|
|
6263
|
+
options.output = "pretty";
|
|
4682
6264
|
} else if (arg === "--trace") {
|
|
4683
6265
|
options.trace = true;
|
|
4684
6266
|
} else if (arg === "-h" || arg === "--help") {
|
|
@@ -4698,7 +6280,7 @@ function output(data, format = "pretty") {
|
|
|
4698
6280
|
} else if (typeof data === "object" && data !== null) {
|
|
4699
6281
|
const { truncated } = prettyPrint(data);
|
|
4700
6282
|
if (truncated) {
|
|
4701
|
-
console.log("\n(Output truncated. Use
|
|
6283
|
+
console.log("\n(Output truncated. Use --json for full data)");
|
|
4702
6284
|
}
|
|
4703
6285
|
} else {
|
|
4704
6286
|
console.log(data);
|
|
@@ -4748,6 +6330,9 @@ async function main() {
|
|
|
4748
6330
|
case "snapshot":
|
|
4749
6331
|
await snapshotCommand(remaining, options);
|
|
4750
6332
|
break;
|
|
6333
|
+
case "diagnose":
|
|
6334
|
+
await diagnoseCommand(remaining, options);
|
|
6335
|
+
break;
|
|
4751
6336
|
case "text":
|
|
4752
6337
|
await textCommand(remaining, options);
|
|
4753
6338
|
break;
|