browser-pilot 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +39 -0
  2. package/dist/actions.cjs +797 -69
  3. package/dist/actions.d.cts +101 -4
  4. package/dist/actions.d.ts +101 -4
  5. package/dist/actions.mjs +17 -1
  6. package/dist/{browser-ZCR6AA4D.mjs → browser-GHQRYU4R.mjs} +2 -2
  7. package/dist/browser.cjs +1366 -72
  8. package/dist/browser.d.cts +230 -6
  9. package/dist/browser.d.ts +230 -6
  10. package/dist/browser.mjs +37 -5
  11. package/dist/{chunk-EZNZ72VA.mjs → chunk-ASEIFKKV.mjs} +126 -0
  12. package/dist/{chunk-TJ5B56NV.mjs → chunk-FSB25GRR.mjs} +129 -1
  13. package/dist/chunk-MIJ7UIKB.mjs +96 -0
  14. package/dist/{chunk-6GBYX7C2.mjs → chunk-MRY3HRFJ.mjs} +799 -353
  15. package/dist/chunk-OIHU7OFY.mjs +91 -0
  16. package/dist/{chunk-NNEHWWHL.mjs → chunk-SW52ALBD.mjs} +588 -5
  17. package/dist/{chunk-V3VLBQAM.mjs → chunk-ZDODXEBD.mjs} +586 -69
  18. package/dist/cli.mjs +784 -176
  19. package/dist/combobox-RAKBA2BW.mjs +6 -0
  20. package/dist/index.cjs +1669 -71
  21. package/dist/index.d.cts +58 -7
  22. package/dist/index.d.ts +58 -7
  23. package/dist/index.mjs +192 -3
  24. package/dist/{page-IUUTJ3SW.mjs → page-SD64DY3F.mjs} +1 -1
  25. package/dist/providers.cjs +127 -0
  26. package/dist/providers.d.cts +38 -3
  27. package/dist/providers.d.ts +38 -3
  28. package/dist/providers.mjs +3 -1
  29. package/dist/{types-BzM-IfsL.d.ts → types-B_v62K7C.d.ts} +146 -2
  30. package/dist/{types-DeVSWhXj.d.cts → types-D2pJQpWs.d.cts} +7 -1
  31. package/dist/{types-DeVSWhXj.d.ts → types-D2pJQpWs.d.ts} +7 -1
  32. package/dist/{types-BflRmiDz.d.cts → types-Yuybzq53.d.cts} +146 -2
  33. package/dist/upload-E6MCC2OF.mjs +6 -0
  34. package/package.json +10 -3
package/dist/browser.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,206 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/browser/combobox.ts
34
+ var combobox_exports = {};
35
+ __export(combobox_exports, {
36
+ chooseOption: () => chooseOption
37
+ });
38
+ async function chooseOption(page, config) {
39
+ const {
40
+ trigger,
41
+ listbox,
42
+ optionSelector,
43
+ searchText,
44
+ value,
45
+ match = "contains",
46
+ timeout = 1e4
47
+ } = config;
48
+ try {
49
+ await page.click(trigger, { timeout });
50
+ const listboxSelectors = listbox ? Array.isArray(listbox) ? listbox : [listbox] : DEFAULT_LISTBOX_SELECTORS;
51
+ const listboxFound = await page.waitFor(listboxSelectors, {
52
+ timeout: Math.min(timeout, 3e3),
53
+ optional: true,
54
+ state: "visible"
55
+ });
56
+ if (!listboxFound) {
57
+ return {
58
+ success: false,
59
+ failedAt: "open",
60
+ error: "Listbox did not appear after clicking trigger"
61
+ };
62
+ }
63
+ if (searchText) {
64
+ try {
65
+ const triggerSel = Array.isArray(trigger) ? trigger[0] : trigger;
66
+ await page.type(triggerSel, searchText, {
67
+ delay: 30,
68
+ timeout: Math.min(timeout, 3e3)
69
+ });
70
+ await new Promise((resolve) => setTimeout(resolve, 200));
71
+ } catch {
72
+ return { success: false, failedAt: "search", error: "Failed to type search text" };
73
+ }
74
+ }
75
+ const optionSelectors = optionSelector ? [optionSelector] : DEFAULT_OPTION_SELECTORS;
76
+ const matchFn = match === "exact" ? "exact" : match === "startsWith" ? "startsWith" : "contains";
77
+ const clickedOption = await page.evaluate(`(() => {
78
+ const selectors = ${JSON.stringify(optionSelectors)};
79
+ const targetValue = ${JSON.stringify(value)};
80
+ const matchMode = ${JSON.stringify(matchFn)};
81
+
82
+ for (const sel of selectors) {
83
+ const options = document.querySelectorAll(sel);
84
+ for (const opt of options) {
85
+ const text = (opt.textContent || '').trim();
86
+ let matches = false;
87
+ if (matchMode === 'exact') matches = text === targetValue;
88
+ else if (matchMode === 'startsWith') matches = text.startsWith(targetValue);
89
+ else matches = text.includes(targetValue);
90
+
91
+ if (matches) {
92
+ opt.click();
93
+ return text;
94
+ }
95
+ }
96
+ }
97
+ return null;
98
+ })()`);
99
+ if (!clickedOption) {
100
+ return { success: false, failedAt: "select", error: `No option matching "${value}" found` };
101
+ }
102
+ return {
103
+ success: true,
104
+ selectedText: String(clickedOption)
105
+ };
106
+ } catch (error) {
107
+ return {
108
+ success: false,
109
+ error: error instanceof Error ? error.message : String(error)
110
+ };
111
+ }
112
+ }
113
+ var DEFAULT_LISTBOX_SELECTORS, DEFAULT_OPTION_SELECTORS;
114
+ var init_combobox = __esm({
115
+ "src/browser/combobox.ts"() {
116
+ "use strict";
117
+ DEFAULT_LISTBOX_SELECTORS = [
118
+ '[role="listbox"]',
119
+ '[role="menu"]',
120
+ '[role="tree"]',
121
+ 'ul[class*="dropdown"]',
122
+ 'ul[class*="option"]',
123
+ 'ul[class*="list"]',
124
+ 'div[class*="dropdown"]',
125
+ 'div[class*="menu"]'
126
+ ];
127
+ DEFAULT_OPTION_SELECTORS = [
128
+ '[role="option"]',
129
+ '[role="menuitem"]',
130
+ '[role="treeitem"]',
131
+ "li"
132
+ ];
133
+ }
134
+ });
135
+
136
+ // src/browser/upload.ts
137
+ var upload_exports = {};
138
+ __export(upload_exports, {
139
+ uploadFiles: () => uploadFiles
140
+ });
141
+ async function uploadFiles(page, config) {
142
+ const { selector, files, timeout = 1e4 } = config;
143
+ const fileNames = files.map((f) => f.split("/").pop() ?? f);
144
+ try {
145
+ const selectors = Array.isArray(selector) ? selector : [selector];
146
+ let nodeId;
147
+ for (const sel of selectors) {
148
+ try {
149
+ const found = await page.waitFor(sel, {
150
+ timeout: Math.min(timeout, 5e3),
151
+ optional: true,
152
+ state: "attached"
153
+ });
154
+ if (found) {
155
+ const result = await page.evaluate(`(() => {
156
+ const el = document.querySelector(${JSON.stringify(sel)});
157
+ if (!el) return null;
158
+ return el.tagName.toLowerCase() === 'input' && el.type === 'file' ? 'file-input' : 'not-file-input';
159
+ })()`);
160
+ if (result === "file-input") {
161
+ const doc = await page.cdpClient.send("DOM.getDocument");
162
+ const queryResult = await page.cdpClient.send("DOM.querySelector", {
163
+ nodeId: doc.root.nodeId,
164
+ selector: sel
165
+ });
166
+ nodeId = queryResult.nodeId;
167
+ break;
168
+ }
169
+ }
170
+ } catch {
171
+ }
172
+ }
173
+ if (!nodeId) {
174
+ return {
175
+ accepted: false,
176
+ fileCount: 0,
177
+ fileNames,
178
+ error: "No file input element found"
179
+ };
180
+ }
181
+ await page.cdpClient.send("DOM.setFileInputFiles", {
182
+ files,
183
+ nodeId
184
+ });
185
+ await new Promise((resolve) => setTimeout(resolve, 300));
186
+ let validationError;
187
+ try {
188
+ const errorText = await page.evaluate(`(() => {
189
+ const errorSelectors = ['.error', '.validation-error', '[class*="error"]', '[role="alert"]'];
190
+ for (const sel of errorSelectors) {
191
+ const el = document.querySelector(sel);
192
+ if (el && el.offsetParent !== null && el.textContent.trim()) {
193
+ return el.textContent.trim();
194
+ }
195
+ }
196
+ return null;
197
+ })()`);
198
+ if (errorText) validationError = String(errorText);
199
+ } catch {
200
+ }
201
+ let visibleInUI;
202
+ try {
203
+ const visible = await page.evaluate(`(() => {
204
+ const text = document.body.innerText;
205
+ const fileNames = ${JSON.stringify(fileNames)};
206
+ return fileNames.some(name => text.includes(name));
207
+ })()`);
208
+ visibleInUI = visible === true;
209
+ } catch {
210
+ }
211
+ return {
212
+ accepted: true,
213
+ fileCount: files.length,
214
+ fileNames,
215
+ visibleInUI,
216
+ validationError
217
+ };
218
+ } catch (error) {
219
+ return {
220
+ accepted: false,
221
+ fileCount: 0,
222
+ fileNames,
223
+ error: error instanceof Error ? error.message : String(error)
224
+ };
225
+ }
226
+ }
227
+ var init_upload = __esm({
228
+ "src/browser/upload.ts"() {
229
+ "use strict";
230
+ }
231
+ });
232
+
30
233
  // src/browser/index.ts
31
234
  var browser_exports = {};
32
235
  __export(browser_exports, {
@@ -35,7 +238,21 @@ __export(browser_exports, {
35
238
  NavigationError: () => NavigationError,
36
239
  Page: () => Page,
37
240
  TimeoutError: () => TimeoutError,
38
- connect: () => connect
241
+ buildFingerprintMap: () => buildFingerprintMap,
242
+ chooseOption: () => chooseOption,
243
+ computeDelta: () => computeDelta,
244
+ connect: () => connect,
245
+ createFingerprint: () => createFingerprint,
246
+ createTargetFingerprint: () => createTargetFingerprint,
247
+ detectOverlay: () => detectOverlay,
248
+ extractPageState: () => extractPageState,
249
+ extractReview: () => extractReview,
250
+ fingerprintKey: () => fingerprintKey,
251
+ fingerprintSimilarity: () => fingerprintSimilarity,
252
+ recoverPinnedTarget: () => recoverPinnedTarget,
253
+ recoverStaleRef: () => recoverStaleRef,
254
+ submitAndVerify: () => submitAndVerify,
255
+ uploadFiles: () => uploadFiles
39
256
  });
40
257
  module.exports = __toCommonJS(browser_exports);
41
258
 
@@ -341,6 +558,108 @@ function buildCDPClient(transport, options = {}) {
341
558
  return client;
342
559
  }
343
560
 
561
+ // src/providers/browser-use.ts
562
+ var BrowserUseProvider = class {
563
+ name = "browser-use";
564
+ apiKey;
565
+ baseUrl;
566
+ proxyCountryCode;
567
+ profileId;
568
+ timeout;
569
+ allowResizing;
570
+ customProxy;
571
+ constructor(options) {
572
+ this.apiKey = options.apiKey;
573
+ this.baseUrl = options.baseUrl ?? "https://api.browser-use.com/api/v2";
574
+ this.proxyCountryCode = options.proxyCountryCode === void 0 ? "uk" : options.proxyCountryCode;
575
+ this.profileId = options.profileId;
576
+ this.timeout = options.timeout;
577
+ this.allowResizing = options.allowResizing;
578
+ this.customProxy = options.customProxy;
579
+ }
580
+ async createSession(options = {}) {
581
+ const body = {};
582
+ body["proxyCountryCode"] = this.proxyCountryCode;
583
+ if (options.width) body["browserScreenWidth"] = options.width;
584
+ if (options.height) body["browserScreenHeight"] = options.height;
585
+ if (this.profileId) body["profileId"] = this.profileId;
586
+ if (this.timeout !== void 0) body["timeout"] = this.timeout;
587
+ if (this.allowResizing !== void 0) body["allowResizing"] = this.allowResizing;
588
+ if (this.customProxy) body["customProxy"] = this.customProxy;
589
+ const response = await fetch(`${this.baseUrl}/browsers`, {
590
+ method: "POST",
591
+ headers: {
592
+ "X-Browser-Use-API-Key": this.apiKey,
593
+ "Content-Type": "application/json"
594
+ },
595
+ body: JSON.stringify(body)
596
+ });
597
+ if (!response.ok) {
598
+ const text = await response.text();
599
+ this.throwApiError(response.status, text);
600
+ }
601
+ const session = await response.json();
602
+ if (!session.cdpUrl) {
603
+ throw new Error("Browser Use session does not have a cdpUrl");
604
+ }
605
+ return this.toProviderSession(session);
606
+ }
607
+ async resumeSession(sessionId) {
608
+ const response = await fetch(`${this.baseUrl}/browsers/${sessionId}`, {
609
+ headers: {
610
+ "X-Browser-Use-API-Key": this.apiKey
611
+ }
612
+ });
613
+ if (!response.ok) {
614
+ const text = await response.text();
615
+ throw new Error(`Browser Use resumeSession failed: ${response.status} ${text}`);
616
+ }
617
+ const session = await response.json();
618
+ if (session.status !== "active" || !session.cdpUrl) {
619
+ throw new Error(
620
+ "Browser Use session is not active or does not have a cdpUrl (may be stopped)"
621
+ );
622
+ }
623
+ return this.toProviderSession(session);
624
+ }
625
+ toProviderSession(session) {
626
+ return {
627
+ wsUrl: session.cdpUrl,
628
+ sessionId: session.id,
629
+ metadata: {
630
+ liveUrl: session.liveUrl,
631
+ status: session.status,
632
+ timeoutAt: session.timeoutAt,
633
+ proxyCountryCode: this.proxyCountryCode
634
+ },
635
+ close: async () => {
636
+ await fetch(`${this.baseUrl}/browsers/${session.id}`, {
637
+ method: "PATCH",
638
+ headers: {
639
+ "X-Browser-Use-API-Key": this.apiKey,
640
+ "Content-Type": "application/json"
641
+ },
642
+ body: JSON.stringify({ action: "stop" })
643
+ });
644
+ }
645
+ };
646
+ }
647
+ throwApiError(status, body) {
648
+ switch (status) {
649
+ case 402:
650
+ throw new Error(`Browser Use: insufficient credits (min $0.10 required). ${body}`);
651
+ case 403:
652
+ throw new Error(`Browser Use: invalid API key. ${body}`);
653
+ case 422:
654
+ throw new Error(`Browser Use: validation error. ${body}`);
655
+ case 429:
656
+ throw new Error(`Browser Use: rate limit exceeded. ${body}`);
657
+ default:
658
+ throw new Error(`Browser Use createSession failed: ${status} ${body}`);
659
+ }
660
+ }
661
+ };
662
+
344
663
  // src/providers/browserbase.ts
345
664
  var BrowserBaseProvider = class {
346
665
  name = "browserbase";
@@ -850,6 +1169,17 @@ async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies)
850
1169
  );
851
1170
  }
852
1171
 
1172
+ // src/runtime/env.ts
1173
+ function getProcessEnv() {
1174
+ if (typeof globalThis.process !== "undefined" && globalThis.process.env) {
1175
+ return globalThis.process.env;
1176
+ }
1177
+ return {};
1178
+ }
1179
+ function getEnv(name) {
1180
+ return getProcessEnv()[name];
1181
+ }
1182
+
853
1183
  // src/providers/index.ts
854
1184
  function createProvider(options) {
855
1185
  switch (options.provider) {
@@ -871,6 +1201,18 @@ function createProvider(options) {
871
1201
  return new BrowserlessProvider({
872
1202
  token: options.apiKey
873
1203
  });
1204
+ case "browser-use": {
1205
+ const apiKey = options.apiKey ?? getEnv("BROWSER_USE_API_KEY");
1206
+ if (!apiKey) {
1207
+ throw new Error("Browser Use provider requires apiKey or BROWSER_USE_API_KEY env var");
1208
+ }
1209
+ return new BrowserUseProvider({
1210
+ apiKey,
1211
+ proxyCountryCode: options.proxyCountryCode === void 0 ? "uk" : options.proxyCountryCode,
1212
+ profileId: options.profileId,
1213
+ timeout: options.cloudTimeout
1214
+ });
1215
+ }
874
1216
  case "generic":
875
1217
  if (!options.wsUrl) {
876
1218
  throw new Error("Generic provider requires wsUrl");
@@ -883,6 +1225,275 @@ function createProvider(options) {
883
1225
  }
884
1226
  }
885
1227
 
1228
+ // src/utils/strings.ts
1229
+ function readString(value) {
1230
+ return typeof value === "string" ? value : void 0;
1231
+ }
1232
+ function readStringOr(value, fallback = "") {
1233
+ return readString(value) ?? fallback;
1234
+ }
1235
+ function formatConsoleArg(entry) {
1236
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
1237
+ }
1238
+ function globToRegex(pattern) {
1239
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1240
+ const withWildcards = escaped.replace(/\*/g, ".*");
1241
+ return new RegExp(`^${withWildcards}$`);
1242
+ }
1243
+
1244
+ // src/actions/conditions.ts
1245
+ var NetworkResponseTracker = class {
1246
+ responses = [];
1247
+ listening = false;
1248
+ handler = null;
1249
+ start(cdp) {
1250
+ if (this.listening) return;
1251
+ this.listening = true;
1252
+ this.handler = (params) => {
1253
+ const response = params["response"];
1254
+ if (response) {
1255
+ this.responses.push({ url: response.url, status: response.status });
1256
+ }
1257
+ };
1258
+ cdp.on("Network.responseReceived", this.handler);
1259
+ }
1260
+ stop(cdp) {
1261
+ if (this.handler) {
1262
+ cdp.off("Network.responseReceived", this.handler);
1263
+ this.handler = null;
1264
+ }
1265
+ this.listening = false;
1266
+ }
1267
+ getResponses() {
1268
+ return this.responses;
1269
+ }
1270
+ reset() {
1271
+ this.responses = [];
1272
+ }
1273
+ };
1274
+ async function captureStateSignature(page) {
1275
+ try {
1276
+ const url = await page.url();
1277
+ const text = await page.text();
1278
+ const truncated = text.slice(0, 2e3);
1279
+ return `${url}|${simpleHash(truncated)}`;
1280
+ } catch {
1281
+ return "";
1282
+ }
1283
+ }
1284
+ function simpleHash(str) {
1285
+ let hash = 0;
1286
+ for (let i = 0; i < str.length; i++) {
1287
+ const char = str.charCodeAt(i);
1288
+ hash = (hash << 5) - hash + char | 0;
1289
+ }
1290
+ return hash.toString(36);
1291
+ }
1292
+ async function evaluateCondition(condition, page, context = {}) {
1293
+ switch (condition.kind) {
1294
+ case "urlMatches": {
1295
+ try {
1296
+ const currentUrl = await page.url();
1297
+ const regex = globToRegex(condition.pattern);
1298
+ const matched = regex.test(currentUrl);
1299
+ return {
1300
+ condition,
1301
+ matched,
1302
+ detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
1303
+ };
1304
+ } catch {
1305
+ return { condition, matched: false, detail: "Failed to get current URL" };
1306
+ }
1307
+ }
1308
+ case "elementVisible": {
1309
+ try {
1310
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
1311
+ for (const sel of selectors) {
1312
+ const visible = await page.waitFor(sel, {
1313
+ timeout: 2e3,
1314
+ optional: true,
1315
+ state: "visible"
1316
+ });
1317
+ if (visible) {
1318
+ return { condition, matched: true, detail: `Element "${sel}" is visible` };
1319
+ }
1320
+ }
1321
+ return { condition, matched: false, detail: "No matching visible element found" };
1322
+ } catch {
1323
+ return { condition, matched: false, detail: "Visibility check failed" };
1324
+ }
1325
+ }
1326
+ case "elementHidden": {
1327
+ try {
1328
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
1329
+ for (const sel of selectors) {
1330
+ const visible = await page.waitFor(sel, {
1331
+ timeout: 500,
1332
+ optional: true,
1333
+ state: "visible"
1334
+ });
1335
+ if (visible) {
1336
+ return { condition, matched: false, detail: `Element "${sel}" is still visible` };
1337
+ }
1338
+ }
1339
+ return { condition, matched: true, detail: "Element is hidden or not found" };
1340
+ } catch {
1341
+ return { condition, matched: true, detail: "Element is hidden (check threw)" };
1342
+ }
1343
+ }
1344
+ case "textAppears": {
1345
+ try {
1346
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
1347
+ const text = await page.text(selector);
1348
+ const matched = text.includes(condition.text);
1349
+ return {
1350
+ condition,
1351
+ matched,
1352
+ detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
1353
+ };
1354
+ } catch {
1355
+ return { condition, matched: false, detail: "Failed to get page text" };
1356
+ }
1357
+ }
1358
+ case "textChanges": {
1359
+ try {
1360
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
1361
+ const text = await page.text(selector);
1362
+ if (condition.to !== void 0) {
1363
+ const matched = text.includes(condition.to);
1364
+ return {
1365
+ condition,
1366
+ matched,
1367
+ detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
1368
+ };
1369
+ }
1370
+ return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
1371
+ } catch {
1372
+ return { condition, matched: false, detail: "Failed to get text for change detection" };
1373
+ }
1374
+ }
1375
+ case "networkResponse": {
1376
+ const tracker = context.networkTracker;
1377
+ if (!tracker) {
1378
+ return { condition, matched: false, detail: "No network tracker active" };
1379
+ }
1380
+ const regex = globToRegex(condition.urlPattern);
1381
+ const responses = tracker.getResponses();
1382
+ for (const resp of responses) {
1383
+ if (regex.test(resp.url)) {
1384
+ if (condition.status !== void 0 && resp.status !== condition.status) {
1385
+ continue;
1386
+ }
1387
+ return {
1388
+ condition,
1389
+ matched: true,
1390
+ detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
1391
+ };
1392
+ }
1393
+ }
1394
+ return {
1395
+ condition,
1396
+ matched: false,
1397
+ detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
1398
+ };
1399
+ }
1400
+ case "stateSignatureChanges": {
1401
+ if (!context.beforeSignature) {
1402
+ return { condition, matched: false, detail: "No before-signature captured" };
1403
+ }
1404
+ const afterSignature = await captureStateSignature(page);
1405
+ const matched = afterSignature !== context.beforeSignature;
1406
+ return {
1407
+ condition,
1408
+ matched,
1409
+ detail: matched ? "Page state changed" : "Page state unchanged"
1410
+ };
1411
+ }
1412
+ default: {
1413
+ const _exhaustive = condition;
1414
+ return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
1415
+ }
1416
+ }
1417
+ }
1418
+ async function evaluateOutcome(page, options) {
1419
+ const {
1420
+ expectAny,
1421
+ expectAll,
1422
+ failIf,
1423
+ dangerous = false,
1424
+ networkTracker,
1425
+ beforeSignature
1426
+ } = options;
1427
+ const allMatched = [];
1428
+ const context = { networkTracker, beforeSignature };
1429
+ if (failIf && failIf.length > 0) {
1430
+ for (const condition of failIf) {
1431
+ const result = await evaluateCondition(condition, page, context);
1432
+ allMatched.push(result);
1433
+ if (result.matched) {
1434
+ return {
1435
+ outcomeStatus: "failed",
1436
+ matchedConditions: allMatched,
1437
+ retrySafe: !dangerous
1438
+ };
1439
+ }
1440
+ }
1441
+ }
1442
+ if (expectAll && expectAll.length > 0) {
1443
+ let allPassed = true;
1444
+ for (const condition of expectAll) {
1445
+ const result = await evaluateCondition(condition, page, context);
1446
+ allMatched.push(result);
1447
+ if (!result.matched) {
1448
+ allPassed = false;
1449
+ }
1450
+ }
1451
+ if (!allPassed) {
1452
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
1453
+ return {
1454
+ outcomeStatus: status,
1455
+ matchedConditions: allMatched,
1456
+ retrySafe: !dangerous
1457
+ };
1458
+ }
1459
+ if (!expectAny || expectAny.length === 0) {
1460
+ return {
1461
+ outcomeStatus: "success",
1462
+ matchedConditions: allMatched,
1463
+ retrySafe: true
1464
+ };
1465
+ }
1466
+ }
1467
+ if (expectAny && expectAny.length > 0) {
1468
+ let anyPassed = false;
1469
+ for (const condition of expectAny) {
1470
+ const result = await evaluateCondition(condition, page, context);
1471
+ allMatched.push(result);
1472
+ if (result.matched) {
1473
+ anyPassed = true;
1474
+ }
1475
+ }
1476
+ if (anyPassed) {
1477
+ return {
1478
+ outcomeStatus: "success",
1479
+ matchedConditions: allMatched,
1480
+ retrySafe: true
1481
+ };
1482
+ }
1483
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
1484
+ return {
1485
+ outcomeStatus: status,
1486
+ matchedConditions: allMatched,
1487
+ retrySafe: !dangerous
1488
+ };
1489
+ }
1490
+ return {
1491
+ outcomeStatus: "success",
1492
+ matchedConditions: allMatched,
1493
+ retrySafe: true
1494
+ };
1495
+ }
1496
+
886
1497
  // src/actions/executor.ts
887
1498
  var fs = __toESM(require("fs"), 1);
888
1499
  var import_node_path = require("path");
@@ -2330,13 +2941,6 @@ var TRACE_SCRIPT = `
2330
2941
  })();
2331
2942
  `;
2332
2943
 
2333
- // src/trace/live.ts
2334
- function globToRegex(pattern) {
2335
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2336
- const withWildcards = escaped.replace(/\*/g, ".*");
2337
- return new RegExp(`^${withWildcards}$`);
2338
- }
2339
-
2340
2944
  // src/actions/executor.ts
2341
2945
  var DEFAULT_TIMEOUT = 3e4;
2342
2946
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -2346,15 +2950,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
2346
2950
  "text",
2347
2951
  "screenshot"
2348
2952
  ];
2349
- function readString(value) {
2350
- return typeof value === "string" ? value : void 0;
2351
- }
2352
- function readStringOr(value, fallback = "") {
2353
- return readString(value) ?? fallback;
2354
- }
2355
- function formatConsoleArg(entry) {
2356
- return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
2357
- }
2358
2953
  function loadExistingRecording(manifestPath) {
2359
2954
  try {
2360
2955
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
@@ -2468,6 +3063,25 @@ function getSuggestion(reason) {
2468
3063
  }
2469
3064
  }
2470
3065
  }
3066
+ function hasOutcomeConditions(step) {
3067
+ return step.expectAny !== void 0 && step.expectAny.length > 0 || step.expectAll !== void 0 && step.expectAll.length > 0 || step.failIf !== void 0 && step.failIf.length > 0;
3068
+ }
3069
+ function needsNetworkTracking(step) {
3070
+ const allConditions = [
3071
+ ...step.expectAny ?? [],
3072
+ ...step.expectAll ?? [],
3073
+ ...step.failIf ?? []
3074
+ ];
3075
+ return allConditions.some((c) => c.kind === "networkResponse");
3076
+ }
3077
+ function needsStateSignature(step) {
3078
+ const allConditions = [
3079
+ ...step.expectAny ?? [],
3080
+ ...step.expectAll ?? [],
3081
+ ...step.failIf ?? []
3082
+ ];
3083
+ return allConditions.some((c) => c.kind === "stateSignatureChanges");
3084
+ }
2471
3085
  var BatchExecutor = class {
2472
3086
  page;
2473
3087
  constructor(page) {
@@ -2513,9 +3127,25 @@ var BatchExecutor = class {
2513
3127
  })
2514
3128
  );
2515
3129
  }
3130
+ const hasOutcome = hasOutcomeConditions(step);
3131
+ let networkTracker;
3132
+ let beforeSignature;
3133
+ if (hasOutcome) {
3134
+ if (needsNetworkTracking(step)) {
3135
+ networkTracker = new NetworkResponseTracker();
3136
+ networkTracker.start(this.page.cdpClient);
3137
+ }
3138
+ if (needsStateSignature(step)) {
3139
+ beforeSignature = await captureStateSignature(this.page);
3140
+ }
3141
+ }
2516
3142
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2517
3143
  if (attempt > 0) {
2518
3144
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
3145
+ if (networkTracker) networkTracker.reset();
3146
+ if (hasOutcome && needsStateSignature(step)) {
3147
+ beforeSignature = await captureStateSignature(this.page);
3148
+ }
2519
3149
  }
2520
3150
  try {
2521
3151
  this.page.resetLastActionPosition();
@@ -2533,6 +3163,28 @@ var BatchExecutor = class {
2533
3163
  coordinates: this.page.getLastActionCoordinates() ?? void 0,
2534
3164
  boundingBox: this.page.getLastActionBoundingBox() ?? void 0
2535
3165
  };
3166
+ if (hasOutcome) {
3167
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
3168
+ const outcome = await evaluateOutcome(this.page, {
3169
+ expectAny: step.expectAny,
3170
+ expectAll: step.expectAll,
3171
+ failIf: step.failIf,
3172
+ dangerous: step.dangerous,
3173
+ networkTracker,
3174
+ beforeSignature
3175
+ });
3176
+ stepResult.outcomeStatus = outcome.outcomeStatus;
3177
+ stepResult.matchedConditions = outcome.matchedConditions;
3178
+ stepResult.retrySafe = outcome.retrySafe;
3179
+ if (outcome.outcomeStatus !== "success") {
3180
+ stepResult.success = false;
3181
+ stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
3182
+ const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
3183
+ if (failedDetails.length > 0) {
3184
+ stepResult.suggestion = failedDetails.join("; ");
3185
+ }
3186
+ }
3187
+ }
2536
3188
  if (recording && !recording.skipActions.has(step.action)) {
2537
3189
  await this.captureRecordingFrame(step, stepResult, recording);
2538
3190
  }
@@ -2542,13 +3194,14 @@ var BatchExecutor = class {
2542
3194
  traceId: createTraceId("action"),
2543
3195
  elapsedMs: Date.now() - startTime,
2544
3196
  channel: "action",
2545
- event: "action.succeeded",
2546
- summary: `${step.action} succeeded`,
3197
+ event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
3198
+ summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
2547
3199
  data: {
2548
3200
  action: step.action,
2549
3201
  selector: step.selector ?? null,
2550
3202
  selectorUsed: result.selectorUsed ?? null,
2551
- durationMs: Date.now() - stepStart
3203
+ durationMs: Date.now() - stepStart,
3204
+ outcomeStatus: stepResult.outcomeStatus ?? null
2552
3205
  },
2553
3206
  actionId: `action-${i + 1}`,
2554
3207
  stepIndex: i,
@@ -2558,6 +3211,18 @@ var BatchExecutor = class {
2558
3211
  })
2559
3212
  );
2560
3213
  }
3214
+ if (hasOutcome && !stepResult.success) {
3215
+ if (step.dangerous) {
3216
+ results.push(stepResult);
3217
+ break;
3218
+ }
3219
+ if (attempt < maxAttempts - 1) {
3220
+ lastError = new Error(stepResult.error ?? "Outcome failed");
3221
+ continue;
3222
+ }
3223
+ results.push(stepResult);
3224
+ break;
3225
+ }
2561
3226
  results.push(stepResult);
2562
3227
  succeeded = true;
2563
3228
  break;
@@ -2565,59 +3230,63 @@ var BatchExecutor = class {
2565
3230
  lastError = error instanceof Error ? error : new Error(String(error));
2566
3231
  }
2567
3232
  }
3233
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
2568
3234
  if (!succeeded) {
2569
- const errorMessage = lastError?.message ?? "Unknown error";
2570
- let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
2571
- const { reason, coveringElement } = classifyFailure(lastError);
2572
- if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
2573
- try {
2574
- const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
2575
- const autoHints = await generateHints(this.page, selectors, step.action, 3);
2576
- if (autoHints.length > 0) {
2577
- hints = autoHints;
3235
+ const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
3236
+ if (!resultAlreadyPushed) {
3237
+ const errorMessage = lastError?.message ?? "Unknown error";
3238
+ let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
3239
+ const { reason, coveringElement } = classifyFailure(lastError);
3240
+ if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
3241
+ try {
3242
+ const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
3243
+ const autoHints = await generateHints(this.page, selectors, step.action, 3);
3244
+ if (autoHints.length > 0) {
3245
+ hints = autoHints;
3246
+ }
3247
+ } catch {
2578
3248
  }
2579
- } catch {
2580
3249
  }
3250
+ const failedResult = {
3251
+ index: i,
3252
+ action: step.action,
3253
+ selector: step.selector,
3254
+ success: false,
3255
+ durationMs: Date.now() - stepStart,
3256
+ error: errorMessage,
3257
+ hints,
3258
+ failureReason: reason,
3259
+ coveringElement,
3260
+ suggestion: getSuggestion(reason),
3261
+ timestamp: Date.now()
3262
+ };
3263
+ if (recording && !recording.skipActions.has(step.action)) {
3264
+ await this.captureRecordingFrame(step, failedResult, recording);
3265
+ }
3266
+ if (recording) {
3267
+ recording.traceEvents.push(
3268
+ normalizeTraceEvent({
3269
+ traceId: createTraceId("action"),
3270
+ elapsedMs: Date.now() - startTime,
3271
+ channel: "action",
3272
+ event: "action.failed",
3273
+ severity: "error",
3274
+ summary: `${step.action} failed: ${errorMessage}`,
3275
+ data: {
3276
+ action: step.action,
3277
+ selector: step.selector ?? null,
3278
+ error: errorMessage,
3279
+ reason
3280
+ },
3281
+ actionId: `action-${i + 1}`,
3282
+ stepIndex: i,
3283
+ selector: step.selector,
3284
+ url: step.url
3285
+ })
3286
+ );
3287
+ }
3288
+ results.push(failedResult);
2581
3289
  }
2582
- const failedResult = {
2583
- index: i,
2584
- action: step.action,
2585
- selector: step.selector,
2586
- success: false,
2587
- durationMs: Date.now() - stepStart,
2588
- error: errorMessage,
2589
- hints,
2590
- failureReason: reason,
2591
- coveringElement,
2592
- suggestion: getSuggestion(reason),
2593
- timestamp: Date.now()
2594
- };
2595
- if (recording && !recording.skipActions.has(step.action)) {
2596
- await this.captureRecordingFrame(step, failedResult, recording);
2597
- }
2598
- if (recording) {
2599
- recording.traceEvents.push(
2600
- normalizeTraceEvent({
2601
- traceId: createTraceId("action"),
2602
- elapsedMs: Date.now() - startTime,
2603
- channel: "action",
2604
- event: "action.failed",
2605
- severity: "error",
2606
- summary: `${step.action} failed: ${errorMessage}`,
2607
- data: {
2608
- action: step.action,
2609
- selector: step.selector ?? null,
2610
- error: errorMessage,
2611
- reason
2612
- },
2613
- actionId: `action-${i + 1}`,
2614
- stepIndex: i,
2615
- selector: step.selector,
2616
- url: step.url
2617
- })
2618
- );
2619
- }
2620
- results.push(failedResult);
2621
3290
  if (onFail === "stop" && !step.optional) {
2622
3291
  stoppedAtIndex = i;
2623
3292
  break;
@@ -2928,6 +3597,14 @@ var BatchExecutor = class {
2928
3597
  case "forms": {
2929
3598
  return { value: await this.page.forms() };
2930
3599
  }
3600
+ case "delta": {
3601
+ const review = await this.page.review();
3602
+ return { value: review };
3603
+ }
3604
+ case "review": {
3605
+ const review = await this.page.review();
3606
+ return { value: review };
3607
+ }
2931
3608
  case "screenshot": {
2932
3609
  const data = await this.page.screenshot({
2933
3610
  format: step.format,
@@ -3078,6 +3755,35 @@ var BatchExecutor = class {
3078
3755
  const media = await this.assertMediaTrackLive(step.kind);
3079
3756
  return { value: media };
3080
3757
  }
3758
+ case "chooseOption": {
3759
+ const { chooseOption: chooseOption2 } = await Promise.resolve().then(() => (init_combobox(), combobox_exports));
3760
+ if (!step.value) throw new Error("chooseOption requires value");
3761
+ const result = await chooseOption2(this.page, {
3762
+ trigger: step.trigger ?? step.selector ?? "",
3763
+ listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
3764
+ value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
3765
+ match: step.match,
3766
+ timeout: step.timeout ?? timeout
3767
+ });
3768
+ if (!result.success) {
3769
+ throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
3770
+ }
3771
+ return { value: result };
3772
+ }
3773
+ case "upload": {
3774
+ const { uploadFiles: uploadFiles2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
3775
+ if (!step.selector) throw new Error("upload requires selector");
3776
+ if (!step.files || step.files.length === 0) throw new Error("upload requires files");
3777
+ const result = await uploadFiles2(this.page, {
3778
+ selector: step.selector,
3779
+ files: step.files,
3780
+ timeout: step.timeout ?? timeout
3781
+ });
3782
+ if (!result.accepted) {
3783
+ throw new Error(result.error ?? "Upload was not accepted");
3784
+ }
3785
+ return { value: result };
3786
+ }
3081
3787
  default: {
3082
3788
  const action = step.action;
3083
3789
  const aliases = {
@@ -5323,6 +6029,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
5323
6029
  });
5324
6030
  }
5325
6031
 
6032
+ // src/browser/delta.ts
6033
+ function extractPageState(url, title, snapshot, forms, pageText) {
6034
+ const headings = [];
6035
+ const buttons = [];
6036
+ const alerts = [];
6037
+ function walkNodes(nodes) {
6038
+ for (const node of nodes) {
6039
+ const role = node.role?.toLowerCase() ?? "";
6040
+ if (role === "heading" && node.name) {
6041
+ headings.push(node.name);
6042
+ }
6043
+ if ((role === "button" || role === "link") && node.name) {
6044
+ const disabled = node.disabled ?? false;
6045
+ buttons.push({ text: node.name, disabled, ref: node.ref });
6046
+ }
6047
+ if (role === "alert" && node.name) {
6048
+ alerts.push(node.name);
6049
+ }
6050
+ if (node.children) {
6051
+ walkNodes(node.children);
6052
+ }
6053
+ }
6054
+ }
6055
+ walkNodes(snapshot.accessibilityTree);
6056
+ const formFields = forms.map((f) => ({
6057
+ label: f.label,
6058
+ name: f.name,
6059
+ id: f.id,
6060
+ value: f.value,
6061
+ type: f.type
6062
+ }));
6063
+ return {
6064
+ url,
6065
+ title,
6066
+ headings,
6067
+ formFields,
6068
+ buttons,
6069
+ alerts,
6070
+ visibleText: pageText.slice(0, 3e3)
6071
+ };
6072
+ }
6073
+ function computeDelta(before, after) {
6074
+ const changes = [];
6075
+ if (before.url !== after.url) {
6076
+ changes.push({ kind: "url", before: before.url, after: after.url });
6077
+ }
6078
+ if (before.title !== after.title) {
6079
+ changes.push({ kind: "title", before: before.title, after: after.title });
6080
+ }
6081
+ const beforeHeadings = new Set(before.headings);
6082
+ const afterHeadings = new Set(after.headings);
6083
+ for (const h of after.headings) {
6084
+ if (!beforeHeadings.has(h)) {
6085
+ changes.push({ kind: "heading_added", after: h });
6086
+ }
6087
+ }
6088
+ for (const h of before.headings) {
6089
+ if (!afterHeadings.has(h)) {
6090
+ changes.push({ kind: "heading_removed", before: h });
6091
+ }
6092
+ }
6093
+ const beforeFieldMap = new Map(
6094
+ before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
6095
+ );
6096
+ for (const af of after.formFields) {
6097
+ const key = af.id ?? af.name ?? af.label ?? "";
6098
+ const bf = beforeFieldMap.get(key);
6099
+ if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
6100
+ changes.push({
6101
+ kind: "field_changed",
6102
+ before: String(bf.value ?? ""),
6103
+ after: String(af.value ?? ""),
6104
+ detail: af.label ?? af.name ?? af.id ?? key
6105
+ });
6106
+ }
6107
+ }
6108
+ const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
6109
+ for (const ab of after.buttons) {
6110
+ const bb = beforeBtnMap.get(ab.text);
6111
+ if (bb && bb.disabled !== ab.disabled) {
6112
+ changes.push({
6113
+ kind: "button_changed",
6114
+ detail: ab.text,
6115
+ before: bb.disabled ? "disabled" : "enabled",
6116
+ after: ab.disabled ? "disabled" : "enabled"
6117
+ });
6118
+ }
6119
+ }
6120
+ const beforeAlerts = new Set(before.alerts);
6121
+ const afterAlerts = new Set(after.alerts);
6122
+ for (const a of after.alerts) {
6123
+ if (!beforeAlerts.has(a)) {
6124
+ changes.push({ kind: "alert_added", after: a });
6125
+ }
6126
+ }
6127
+ for (const a of before.alerts) {
6128
+ if (!afterAlerts.has(a)) {
6129
+ changes.push({ kind: "alert_removed", before: a });
6130
+ }
6131
+ }
6132
+ return {
6133
+ changes,
6134
+ before,
6135
+ after,
6136
+ hasChanges: changes.length > 0
6137
+ };
6138
+ }
6139
+
5326
6140
  // src/browser/keyboard.ts
5327
6141
  var US_KEYBOARD = {
5328
6142
  // Letters (lowercase)
@@ -5482,8 +6296,118 @@ function parseShortcut(combo) {
5482
6296
  return { modifiers, key };
5483
6297
  }
5484
6298
 
6299
+ // src/browser/review.ts
6300
+ function extractReview(url, title, snapshot, forms, pageText) {
6301
+ const headings = [];
6302
+ const alerts = [];
6303
+ const statusLabels = [];
6304
+ const keyValues = [];
6305
+ const tables = [];
6306
+ const summaryCards = [];
6307
+ function walkNodes(nodes, parentHeading) {
6308
+ let currentHeading = parentHeading;
6309
+ for (const node of nodes) {
6310
+ const role = node.role?.toLowerCase() ?? "";
6311
+ if (role === "heading" && node.name) {
6312
+ headings.push(node.name);
6313
+ currentHeading = node.name;
6314
+ }
6315
+ if (role === "alert" && node.name) {
6316
+ alerts.push(node.name);
6317
+ }
6318
+ if (role === "status" && node.name) {
6319
+ statusLabels.push(node.name);
6320
+ }
6321
+ if (role === "table" || role === "grid") {
6322
+ const table = extractTableFromNode(node);
6323
+ if (table) tables.push(table);
6324
+ }
6325
+ if ((role === "definition" || role === "term") && node.name) {
6326
+ if (role === "term") {
6327
+ keyValues.push({ key: node.name, value: "" });
6328
+ } else if (role === "definition" && keyValues.length > 0) {
6329
+ const last = keyValues[keyValues.length - 1];
6330
+ if (!last.value) last.value = node.name;
6331
+ }
6332
+ }
6333
+ if (node.children) {
6334
+ walkNodes(node.children, currentHeading);
6335
+ }
6336
+ }
6337
+ }
6338
+ walkNodes(snapshot.accessibilityTree);
6339
+ const textKvPairs = extractKeyValueFromText(pageText);
6340
+ keyValues.push(...textKvPairs);
6341
+ const formEntries = forms.map((f) => ({
6342
+ label: f.label,
6343
+ value: f.value,
6344
+ type: f.type,
6345
+ disabled: f.disabled
6346
+ }));
6347
+ return {
6348
+ url,
6349
+ title,
6350
+ headings,
6351
+ forms: formEntries,
6352
+ alerts,
6353
+ summaryCards,
6354
+ tables,
6355
+ keyValues,
6356
+ statusLabels
6357
+ };
6358
+ }
6359
+ function extractTableFromNode(node) {
6360
+ const headers = [];
6361
+ const rows = [];
6362
+ function findRows(n) {
6363
+ const role = n.role?.toLowerCase() ?? "";
6364
+ if (role === "columnheader" && n.name) {
6365
+ headers.push(n.name);
6366
+ }
6367
+ if (role === "row") {
6368
+ const cells = [];
6369
+ if (n.children) {
6370
+ for (const child of n.children) {
6371
+ const childRole = child.role?.toLowerCase() ?? "";
6372
+ if ((childRole === "cell" || childRole === "gridcell") && child.name) {
6373
+ cells.push(child.name);
6374
+ }
6375
+ }
6376
+ }
6377
+ if (cells.length > 0) rows.push(cells);
6378
+ }
6379
+ if (n.children) {
6380
+ for (const child of n.children) findRows(child);
6381
+ }
6382
+ }
6383
+ findRows(node);
6384
+ if (rows.length === 0) return null;
6385
+ return { headers, rows };
6386
+ }
6387
+ function extractKeyValueFromText(text) {
6388
+ const pairs = [];
6389
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
6390
+ for (const line of lines) {
6391
+ const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
6392
+ if (match) {
6393
+ pairs.push({ key: match[1].trim(), value: match[2].trim() });
6394
+ }
6395
+ }
6396
+ return pairs.slice(0, 20);
6397
+ }
6398
+
5485
6399
  // src/browser/page.ts
5486
6400
  var DEFAULT_TIMEOUT2 = 3e4;
6401
+ function normalizeAXCheckedValue(value) {
6402
+ if (typeof value === "boolean") {
6403
+ return value;
6404
+ }
6405
+ if (typeof value === "string") {
6406
+ if (value === "true") return true;
6407
+ if (value === "false") return false;
6408
+ }
6409
+ return void 0;
6410
+ }
5487
6411
  var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
5488
6412
  if (globalThis.__bpEventListenerTrackerInstalled) return;
5489
6413
  Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
@@ -7269,7 +8193,9 @@ var Page = class {
7269
8193
  }
7270
8194
  }
7271
8195
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7272
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
8196
+ const checked = normalizeAXCheckedValue(
8197
+ node.properties?.find((p) => p.name === "checked")?.value.value
8198
+ );
7273
8199
  return {
7274
8200
  role,
7275
8201
  name,
@@ -7325,7 +8251,9 @@ var Page = class {
7325
8251
  const ref = nodeRefs.get(node.nodeId);
7326
8252
  const name = node.name?.value ?? "";
7327
8253
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7328
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
8254
+ const checked = normalizeAXCheckedValue(
8255
+ node.properties?.find((p) => p.name === "checked")?.value.value
8256
+ );
7329
8257
  const value = node.value?.value;
7330
8258
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
7331
8259
  interactiveElements.push({
@@ -7392,6 +8320,45 @@ var Page = class {
7392
8320
  }
7393
8321
  }
7394
8322
  }
8323
+ // ============ Delta & Review ============
8324
+ /**
8325
+ * Capture current page state for delta comparison.
8326
+ * Call before an action, then call delta() again after and use computeDelta().
8327
+ */
8328
+ async captureState() {
8329
+ const [url, title, snapshot, forms, text] = await Promise.all([
8330
+ this.url(),
8331
+ this.title(),
8332
+ this.snapshot(),
8333
+ this.forms(),
8334
+ this.text()
8335
+ ]);
8336
+ return extractPageState(url, title, snapshot, forms, text);
8337
+ }
8338
+ /**
8339
+ * Compute what changed between two page states.
8340
+ * If no arguments: captures current state and returns it (for use as "before").
8341
+ * If one argument (before state): captures current state and computes delta.
8342
+ */
8343
+ async delta(before) {
8344
+ const currentState = await this.captureState();
8345
+ if (!before) return currentState;
8346
+ return computeDelta(before, currentState);
8347
+ }
8348
+ /**
8349
+ * Extract structured review surface from the current page.
8350
+ * Returns headings, form values, alerts, key-value pairs, tables, and status labels.
8351
+ */
8352
+ async review() {
8353
+ const [url, title, snapshot, forms, text] = await Promise.all([
8354
+ this.url(),
8355
+ this.title(),
8356
+ this.snapshot(),
8357
+ this.forms(),
8358
+ this.text()
8359
+ ]);
8360
+ return extractReview(url, title, snapshot, forms, text);
8361
+ }
7395
8362
  // ============ Batch Execution ============
7396
8363
  /**
7397
8364
  * Execute a batch of steps
@@ -8632,6 +9599,9 @@ var Browser = class _Browser {
8632
9599
  }
8633
9600
  const provider = createProvider(connectOptions);
8634
9601
  const session = await provider.createSession(connectOptions.session);
9602
+ if (session.metadata?.["liveUrl"]) {
9603
+ console.error(`Live viewer: ${session.metadata["liveUrl"]}`);
9604
+ }
8635
9605
  const cdp = await createCDPClient(session.wsUrl, {
8636
9606
  debug: connectOptions.debug,
8637
9607
  timeout: connectOptions.timeout
@@ -8816,6 +9786,316 @@ var Browser = class _Browser {
8816
9786
  function connect(options) {
8817
9787
  return Browser.connect(options);
8818
9788
  }
9789
+
9790
+ // src/browser/index.ts
9791
+ init_combobox();
9792
+
9793
+ // src/browser/fingerprint.ts
9794
+ function createFingerprint(node, context) {
9795
+ const role = node.role?.toLowerCase() ?? "";
9796
+ const name = node.name ?? "";
9797
+ let valueShape = "";
9798
+ if (node.value !== void 0) {
9799
+ valueShape = typeof node.value === "string" ? "text" : typeof node.value === "number" ? "number" : typeof node.value === "boolean" ? "boolean" : "other";
9800
+ }
9801
+ const stableAttrs = {};
9802
+ if (node.properties) {
9803
+ for (const key of ["id", "name", "type", "aria-label"]) {
9804
+ const val = node.properties[key];
9805
+ if (val !== void 0 && val !== null) {
9806
+ stableAttrs[key] = String(val);
9807
+ }
9808
+ }
9809
+ }
9810
+ return {
9811
+ role,
9812
+ name,
9813
+ valueShape,
9814
+ label: name,
9815
+ // label is typically the accessible name
9816
+ stableAttrs,
9817
+ nearestHeading: context.nearestHeading,
9818
+ siblingIndex: context.siblingIndex,
9819
+ sectionPath: [...context.headingTrail]
9820
+ };
9821
+ }
9822
+ function fingerprintKey(fp) {
9823
+ const parts = [fp.role, fp.name, fp.sectionPath.join(">")];
9824
+ if (fp.stableAttrs["id"]) parts.push(`id=${fp.stableAttrs["id"]}`);
9825
+ if (fp.stableAttrs["name"]) parts.push(`name=${fp.stableAttrs["name"]}`);
9826
+ return parts.join("|");
9827
+ }
9828
+ function fingerprintSimilarity(a, b) {
9829
+ let score = 0;
9830
+ let weight = 0;
9831
+ weight += 3;
9832
+ if (a.role === b.role) score += 3;
9833
+ else return 0;
9834
+ weight += 5;
9835
+ if (a.name && b.name && a.name === b.name) score += 5;
9836
+ else if (a.name && b.name && a.name.toLowerCase() === b.name.toLowerCase()) score += 4;
9837
+ weight += 3;
9838
+ const pathA = a.sectionPath.join(">");
9839
+ const pathB = b.sectionPath.join(">");
9840
+ if (pathA === pathB) score += 3;
9841
+ else if (pathA && pathB && (pathA.includes(pathB) || pathB.includes(pathA))) score += 1;
9842
+ const attrKeys = /* @__PURE__ */ new Set([...Object.keys(a.stableAttrs), ...Object.keys(b.stableAttrs)]);
9843
+ for (const key of attrKeys) {
9844
+ weight += 2;
9845
+ if (a.stableAttrs[key] && b.stableAttrs[key] && a.stableAttrs[key] === b.stableAttrs[key]) {
9846
+ score += 2;
9847
+ }
9848
+ }
9849
+ weight += 1;
9850
+ if (a.siblingIndex === b.siblingIndex) score += 1;
9851
+ return score / weight;
9852
+ }
9853
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
9854
+ "button",
9855
+ "link",
9856
+ "textbox",
9857
+ "checkbox",
9858
+ "radio",
9859
+ "combobox",
9860
+ "listbox",
9861
+ "menuitem",
9862
+ "tab",
9863
+ "switch",
9864
+ "searchbox",
9865
+ "spinbutton",
9866
+ "slider"
9867
+ ]);
9868
+ function buildFingerprintMap(nodes) {
9869
+ const map = /* @__PURE__ */ new Map();
9870
+ function walk(nodeList, headingTrail, nearestHeading) {
9871
+ const roleCounts = /* @__PURE__ */ new Map();
9872
+ for (const node of nodeList) {
9873
+ const role = node.role?.toLowerCase() ?? "";
9874
+ let currentHeadingTrail = headingTrail;
9875
+ let currentNearestHeading = nearestHeading;
9876
+ if (role === "heading" && node.name) {
9877
+ currentHeadingTrail = [...headingTrail, node.name];
9878
+ currentNearestHeading = node.name;
9879
+ }
9880
+ if (INTERACTIVE_ROLES.has(role) && node.ref) {
9881
+ const siblingCount = roleCounts.get(role) ?? 0;
9882
+ roleCounts.set(role, siblingCount + 1);
9883
+ const fp = createFingerprint(node, {
9884
+ headingTrail: currentHeadingTrail,
9885
+ siblingIndex: siblingCount,
9886
+ nearestHeading: currentNearestHeading
9887
+ });
9888
+ map.set(node.ref, fp);
9889
+ }
9890
+ if (node.children) {
9891
+ walk(node.children, currentHeadingTrail, currentNearestHeading);
9892
+ }
9893
+ }
9894
+ }
9895
+ walk(nodes, [], "");
9896
+ return map;
9897
+ }
9898
+ function recoverStaleRef(staleFingerprint, currentFingerprints, threshold = 0.7) {
9899
+ let bestRef = null;
9900
+ let bestScore = 0;
9901
+ let secondBestScore = 0;
9902
+ for (const [ref, fp] of currentFingerprints) {
9903
+ const similarity = fingerprintSimilarity(staleFingerprint, fp);
9904
+ if (similarity > bestScore) {
9905
+ secondBestScore = bestScore;
9906
+ bestScore = similarity;
9907
+ bestRef = ref;
9908
+ } else if (similarity > secondBestScore) {
9909
+ secondBestScore = similarity;
9910
+ }
9911
+ }
9912
+ if (!bestRef || bestScore < threshold) return null;
9913
+ if (secondBestScore > 0 && bestScore - secondBestScore < 0.15) return null;
9914
+ return { ref: bestRef, confidence: bestScore };
9915
+ }
9916
+
9917
+ // src/browser/overlay-detect.ts
9918
+ async function detectOverlay(page) {
9919
+ const result = await page.evaluate(`(() => {
9920
+ // Check for role="dialog" or role="alertdialog"
9921
+ const dialogs = document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]');
9922
+ for (const d of dialogs) {
9923
+ if (d.offsetParent !== null || getComputedStyle(d).display !== 'none') {
9924
+ return {
9925
+ hasOverlay: true,
9926
+ overlaySelector: d.id ? '#' + d.id : (d.getAttribute('role') ? '[role="' + d.getAttribute('role') + '"]' : 'dialog'),
9927
+ overlayText: (d.textContent || '').trim().slice(0, 200),
9928
+ };
9929
+ }
9930
+ }
9931
+
9932
+ // Check for fixed/absolute positioned elements with high z-index that look like modals
9933
+ const allElements = document.querySelectorAll('*');
9934
+ for (const el of allElements) {
9935
+ const style = getComputedStyle(el);
9936
+ if (
9937
+ (style.position === 'fixed' || style.position === 'absolute') &&
9938
+ parseInt(style.zIndex || '0', 10) > 999 &&
9939
+ el.offsetWidth > 100 &&
9940
+ el.offsetHeight > 100 &&
9941
+ style.display !== 'none' &&
9942
+ style.visibility !== 'hidden'
9943
+ ) {
9944
+ const text = (el.textContent || '').trim();
9945
+ if (text.length > 10) {
9946
+ return {
9947
+ hasOverlay: true,
9948
+ overlaySelector: el.id ? '#' + el.id : null,
9949
+ overlayText: text.slice(0, 200),
9950
+ };
9951
+ }
9952
+ }
9953
+ }
9954
+
9955
+ return { hasOverlay: false };
9956
+ })()`);
9957
+ return result ?? { hasOverlay: false };
9958
+ }
9959
+
9960
+ // src/browser/safe-submit.ts
9961
+ async function submitAndVerify(page, options) {
9962
+ const {
9963
+ selector,
9964
+ method = "enter+click",
9965
+ expectAny,
9966
+ expectAll,
9967
+ failIf,
9968
+ dangerous = false,
9969
+ timeout = 3e4,
9970
+ waitForNavigation: waitForNavigation2 = "auto"
9971
+ } = options;
9972
+ const startTime = Date.now();
9973
+ const allConditions = [...expectAny ?? [], ...expectAll ?? [], ...failIf ?? []];
9974
+ const needsNetwork = allConditions.some((c) => c.kind === "networkResponse");
9975
+ const needsSignature = allConditions.some((c) => c.kind === "stateSignatureChanges");
9976
+ let networkTracker;
9977
+ let beforeSignature;
9978
+ if (needsNetwork) {
9979
+ networkTracker = new NetworkResponseTracker();
9980
+ networkTracker.start(page.cdpClient);
9981
+ }
9982
+ if (needsSignature) {
9983
+ beforeSignature = await captureStateSignature(page);
9984
+ }
9985
+ try {
9986
+ await page.submit(selector, {
9987
+ timeout,
9988
+ method,
9989
+ waitForNavigation: waitForNavigation2
9990
+ });
9991
+ if (networkTracker) networkTracker.stop(page.cdpClient);
9992
+ if (allConditions.length === 0) {
9993
+ return {
9994
+ submitted: true,
9995
+ outcomeStatus: "success",
9996
+ matchedConditions: [],
9997
+ retrySafe: !dangerous,
9998
+ durationMs: Date.now() - startTime
9999
+ };
10000
+ }
10001
+ const outcome = await evaluateOutcome(page, {
10002
+ expectAny,
10003
+ expectAll,
10004
+ failIf,
10005
+ dangerous,
10006
+ networkTracker,
10007
+ beforeSignature
10008
+ });
10009
+ return {
10010
+ submitted: true,
10011
+ outcomeStatus: outcome.outcomeStatus,
10012
+ matchedConditions: outcome.matchedConditions,
10013
+ retrySafe: outcome.retrySafe,
10014
+ durationMs: Date.now() - startTime
10015
+ };
10016
+ } catch (error) {
10017
+ if (networkTracker) networkTracker.stop(page.cdpClient);
10018
+ return {
10019
+ submitted: false,
10020
+ outcomeStatus: "failed",
10021
+ matchedConditions: [],
10022
+ retrySafe: !dangerous,
10023
+ durationMs: Date.now() - startTime,
10024
+ error: error instanceof Error ? error.message : String(error)
10025
+ };
10026
+ }
10027
+ }
10028
+
10029
+ // src/runtime/clock.ts
10030
+ function now() {
10031
+ return Date.now();
10032
+ }
10033
+
10034
+ // src/browser/target-pin.ts
10035
+ function createTargetFingerprint(targetId, url, title) {
10036
+ return {
10037
+ url,
10038
+ title,
10039
+ originalTargetId: targetId,
10040
+ pinnedAt: now()
10041
+ };
10042
+ }
10043
+ function scoreCandidate(candidate, pin) {
10044
+ if (candidate.targetId === pin.originalTargetId) return 1;
10045
+ let score = 0;
10046
+ if (candidate.url && pin.url) {
10047
+ if (candidate.url === pin.url) {
10048
+ score += 0.6;
10049
+ } else {
10050
+ try {
10051
+ const candidateOrigin = new URL(candidate.url).origin;
10052
+ const pinOrigin = new URL(pin.url).origin;
10053
+ if (candidateOrigin === pinOrigin) score += 0.3;
10054
+ } catch {
10055
+ }
10056
+ }
10057
+ }
10058
+ if (candidate.title && pin.title) {
10059
+ if (candidate.title === pin.title) {
10060
+ score += 0.3;
10061
+ } else if (candidate.title.includes(pin.title) || pin.title.includes(candidate.title)) {
10062
+ score += 0.15;
10063
+ }
10064
+ }
10065
+ if (candidate.type !== "page") score *= 0.5;
10066
+ return Math.min(score, 0.95);
10067
+ }
10068
+ function recoverPinnedTarget(pin, targets, threshold = 0.4) {
10069
+ if (targets.length === 0) return null;
10070
+ let bestTarget = null;
10071
+ let bestScore = 0;
10072
+ for (const target of targets) {
10073
+ const score = scoreCandidate(target, pin);
10074
+ if (score > bestScore) {
10075
+ bestScore = score;
10076
+ bestTarget = target;
10077
+ }
10078
+ }
10079
+ if (!bestTarget || bestScore < threshold) return null;
10080
+ let method;
10081
+ if (bestTarget.targetId === pin.originalTargetId) {
10082
+ method = "exact";
10083
+ } else if (bestTarget.url === pin.url) {
10084
+ method = "url_match";
10085
+ } else if (bestTarget.title === pin.title) {
10086
+ method = "title_match";
10087
+ } else {
10088
+ method = "best_guess";
10089
+ }
10090
+ return {
10091
+ targetId: bestTarget.targetId,
10092
+ method,
10093
+ confidence: bestScore
10094
+ };
10095
+ }
10096
+
10097
+ // src/browser/index.ts
10098
+ init_upload();
8819
10099
  // Annotate the CommonJS export names for ESM import in node:
8820
10100
  0 && (module.exports = {
8821
10101
  Browser,
@@ -8823,5 +10103,19 @@ function connect(options) {
8823
10103
  NavigationError,
8824
10104
  Page,
8825
10105
  TimeoutError,
8826
- connect
10106
+ buildFingerprintMap,
10107
+ chooseOption,
10108
+ computeDelta,
10109
+ connect,
10110
+ createFingerprint,
10111
+ createTargetFingerprint,
10112
+ detectOverlay,
10113
+ extractPageState,
10114
+ extractReview,
10115
+ fingerprintKey,
10116
+ fingerprintSimilarity,
10117
+ recoverPinnedTarget,
10118
+ recoverStaleRef,
10119
+ submitAndVerify,
10120
+ uploadFiles
8827
10121
  });