browser-pilot 0.0.15 → 0.0.17

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 (37) hide show
  1. package/README.md +38 -3
  2. package/dist/actions.cjs +848 -105
  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-MEWT75IB.mjs → browser-4ZHNAQR5.mjs} +2 -2
  7. package/dist/browser.cjs +1684 -130
  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 +563 -0
  12. package/dist/{chunk-ZAXQ5OTV.mjs → chunk-FEEGNSHB.mjs} +606 -12
  13. package/dist/{chunk-WPNW23CE.mjs → chunk-IRLHCVNH.mjs} +345 -7
  14. package/dist/chunk-MIJ7UIKB.mjs +96 -0
  15. package/dist/{chunk-USYSHCI3.mjs → chunk-MRY3HRFJ.mjs} +841 -370
  16. package/dist/chunk-OIHU7OFY.mjs +91 -0
  17. package/dist/{chunk-7YVCOL2W.mjs → chunk-ZDODXEBD.mjs} +637 -105
  18. package/dist/cli.mjs +1280 -549
  19. package/dist/combobox-RAKBA2BW.mjs +6 -0
  20. package/dist/index.cjs +1976 -144
  21. package/dist/index.d.cts +57 -6
  22. package/dist/index.d.ts +57 -6
  23. package/dist/index.mjs +206 -7
  24. package/dist/{page-XPS6IC6V.mjs → page-SD64DY3F.mjs} +1 -1
  25. package/dist/providers.cjs +637 -2
  26. package/dist/providers.d.cts +2 -2
  27. package/dist/providers.d.ts +2 -2
  28. package/dist/providers.mjs +17 -3
  29. package/dist/{types-Cvvf0oGu.d.ts → types-B_v62K7C.d.ts} +147 -3
  30. package/dist/types-DeVSWhXj.d.cts +142 -0
  31. package/dist/types-DeVSWhXj.d.ts +142 -0
  32. package/dist/{types-C9ySEdOX.d.cts → types-Yuybzq53.d.cts} +147 -3
  33. package/dist/upload-E6MCC2OF.mjs +6 -0
  34. package/package.json +10 -3
  35. package/dist/chunk-BRAFQUMG.mjs +0 -229
  36. package/dist/types--wXNHUwt.d.cts +0 -56
  37. package/dist/types--wXNHUwt.d.ts +0 -56
package/dist/index.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/index.ts
31
234
  var src_exports = {};
32
235
  __export(src_exports, {
@@ -35,25 +238,48 @@ __export(src_exports, {
35
238
  BatchExecutor: () => BatchExecutor,
36
239
  Browser: () => Browser,
37
240
  BrowserBaseProvider: () => BrowserBaseProvider,
241
+ BrowserEndpointResolutionError: () => BrowserEndpointResolutionError,
38
242
  BrowserlessProvider: () => BrowserlessProvider,
39
243
  CDPError: () => CDPError,
40
244
  ElementNotFoundError: () => ElementNotFoundError,
41
245
  GenericProvider: () => GenericProvider,
42
246
  NavigationError: () => NavigationError,
247
+ NetworkResponseTracker: () => NetworkResponseTracker,
43
248
  Page: () => Page,
44
249
  RequestInterceptor: () => RequestInterceptor,
45
250
  TimeoutError: () => TimeoutError,
46
251
  Tracer: () => Tracer,
47
252
  addBatchToPage: () => addBatchToPage,
48
253
  bufferToBase64: () => bufferToBase64,
254
+ buildFingerprintMap: () => buildFingerprintMap,
255
+ buildLocalBrowserScanTargets: () => buildLocalBrowserScanTargets,
256
+ buildWorkflowSummary: () => buildWorkflowSummary,
49
257
  calculateRMS: () => calculateRMS,
258
+ captureStateSignature: () => captureStateSignature,
259
+ chooseOption: () => chooseOption,
260
+ computeDelta: () => computeDelta,
261
+ conditionAll: () => conditionAll,
262
+ conditionAny: () => conditionAny,
263
+ conditionNot: () => conditionNot,
264
+ conditionRace: () => conditionRace,
50
265
  connect: () => connect,
51
266
  createCDPClient: () => createCDPClient,
267
+ createFingerprint: () => createFingerprint,
52
268
  createProvider: () => createProvider,
269
+ createTargetFingerprint: () => createTargetFingerprint,
270
+ detectOverlay: () => detectOverlay,
53
271
  devices: () => devices,
54
272
  disableTracing: () => disableTracing,
273
+ discoverLocalBrowsers: () => discoverLocalBrowsers,
55
274
  discoverTargets: () => discoverTargets,
56
275
  enableTracing: () => enableTracing,
276
+ evaluateCondition: () => evaluateCondition,
277
+ evaluateOutcome: () => evaluateOutcome,
278
+ extractPageState: () => extractPageState,
279
+ extractReview: () => extractReview,
280
+ fingerprintKey: () => fingerprintKey,
281
+ fingerprintSimilarity: () => fingerprintSimilarity,
282
+ formatWorkflowSummary: () => formatWorkflowSummary,
57
283
  generateSilence: () => generateSilence,
58
284
  generateTone: () => generateTone,
59
285
  getAudioChromeFlags: () => getAudioChromeFlags,
@@ -61,9 +287,16 @@ __export(src_exports, {
61
287
  getTracer: () => getTracer,
62
288
  grantAudioPermissions: () => grantAudioPermissions,
63
289
  isTranscriptionAvailable: () => isTranscriptionAvailable,
290
+ parseDevToolsActivePortFile: () => parseDevToolsActivePortFile,
64
291
  parseWavHeader: () => parseWavHeader,
65
292
  pcmToWav: () => pcmToWav,
293
+ recoverPinnedTarget: () => recoverPinnedTarget,
294
+ recoverStaleRef: () => recoverStaleRef,
295
+ resolveBrowserEndpoint: () => resolveBrowserEndpoint,
296
+ resolveChromeUserDataDirs: () => resolveChromeUserDataDirs,
297
+ submitAndVerify: () => submitAndVerify,
66
298
  transcribe: () => transcribe,
299
+ uploadFiles: () => uploadFiles,
67
300
  validateSteps: () => validateSteps,
68
301
  waitForAnyElement: () => waitForAnyElement,
69
302
  waitForElement: () => waitForElement,
@@ -72,6 +305,333 @@ __export(src_exports, {
72
305
  });
73
306
  module.exports = __toCommonJS(src_exports);
74
307
 
308
+ // src/utils/strings.ts
309
+ function readString(value) {
310
+ return typeof value === "string" ? value : void 0;
311
+ }
312
+ function readStringOr(value, fallback = "") {
313
+ return readString(value) ?? fallback;
314
+ }
315
+ function formatConsoleArg(entry) {
316
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
317
+ }
318
+ function globToRegex(pattern) {
319
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
320
+ const withWildcards = escaped.replace(/\*/g, ".*");
321
+ return new RegExp(`^${withWildcards}$`);
322
+ }
323
+
324
+ // src/actions/conditions.ts
325
+ var NetworkResponseTracker = class {
326
+ responses = [];
327
+ listening = false;
328
+ handler = null;
329
+ start(cdp) {
330
+ if (this.listening) return;
331
+ this.listening = true;
332
+ this.handler = (params) => {
333
+ const response = params["response"];
334
+ if (response) {
335
+ this.responses.push({ url: response.url, status: response.status });
336
+ }
337
+ };
338
+ cdp.on("Network.responseReceived", this.handler);
339
+ }
340
+ stop(cdp) {
341
+ if (this.handler) {
342
+ cdp.off("Network.responseReceived", this.handler);
343
+ this.handler = null;
344
+ }
345
+ this.listening = false;
346
+ }
347
+ getResponses() {
348
+ return this.responses;
349
+ }
350
+ reset() {
351
+ this.responses = [];
352
+ }
353
+ };
354
+ async function captureStateSignature(page) {
355
+ try {
356
+ const url = await page.url();
357
+ const text = await page.text();
358
+ const truncated = text.slice(0, 2e3);
359
+ return `${url}|${simpleHash(truncated)}`;
360
+ } catch {
361
+ return "";
362
+ }
363
+ }
364
+ function simpleHash(str) {
365
+ let hash = 0;
366
+ for (let i = 0; i < str.length; i++) {
367
+ const char = str.charCodeAt(i);
368
+ hash = (hash << 5) - hash + char | 0;
369
+ }
370
+ return hash.toString(36);
371
+ }
372
+ async function evaluateCondition(condition, page, context = {}) {
373
+ switch (condition.kind) {
374
+ case "urlMatches": {
375
+ try {
376
+ const currentUrl = await page.url();
377
+ const regex = globToRegex(condition.pattern);
378
+ const matched = regex.test(currentUrl);
379
+ return {
380
+ condition,
381
+ matched,
382
+ detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
383
+ };
384
+ } catch {
385
+ return { condition, matched: false, detail: "Failed to get current URL" };
386
+ }
387
+ }
388
+ case "elementVisible": {
389
+ try {
390
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
391
+ for (const sel of selectors) {
392
+ const visible = await page.waitFor(sel, {
393
+ timeout: 2e3,
394
+ optional: true,
395
+ state: "visible"
396
+ });
397
+ if (visible) {
398
+ return { condition, matched: true, detail: `Element "${sel}" is visible` };
399
+ }
400
+ }
401
+ return { condition, matched: false, detail: "No matching visible element found" };
402
+ } catch {
403
+ return { condition, matched: false, detail: "Visibility check failed" };
404
+ }
405
+ }
406
+ case "elementHidden": {
407
+ try {
408
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
409
+ for (const sel of selectors) {
410
+ const visible = await page.waitFor(sel, {
411
+ timeout: 500,
412
+ optional: true,
413
+ state: "visible"
414
+ });
415
+ if (visible) {
416
+ return { condition, matched: false, detail: `Element "${sel}" is still visible` };
417
+ }
418
+ }
419
+ return { condition, matched: true, detail: "Element is hidden or not found" };
420
+ } catch {
421
+ return { condition, matched: true, detail: "Element is hidden (check threw)" };
422
+ }
423
+ }
424
+ case "textAppears": {
425
+ try {
426
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
427
+ const text = await page.text(selector);
428
+ const matched = text.includes(condition.text);
429
+ return {
430
+ condition,
431
+ matched,
432
+ detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
433
+ };
434
+ } catch {
435
+ return { condition, matched: false, detail: "Failed to get page text" };
436
+ }
437
+ }
438
+ case "textChanges": {
439
+ try {
440
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
441
+ const text = await page.text(selector);
442
+ if (condition.to !== void 0) {
443
+ const matched = text.includes(condition.to);
444
+ return {
445
+ condition,
446
+ matched,
447
+ detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
448
+ };
449
+ }
450
+ return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
451
+ } catch {
452
+ return { condition, matched: false, detail: "Failed to get text for change detection" };
453
+ }
454
+ }
455
+ case "networkResponse": {
456
+ const tracker = context.networkTracker;
457
+ if (!tracker) {
458
+ return { condition, matched: false, detail: "No network tracker active" };
459
+ }
460
+ const regex = globToRegex(condition.urlPattern);
461
+ const responses = tracker.getResponses();
462
+ for (const resp of responses) {
463
+ if (regex.test(resp.url)) {
464
+ if (condition.status !== void 0 && resp.status !== condition.status) {
465
+ continue;
466
+ }
467
+ return {
468
+ condition,
469
+ matched: true,
470
+ detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
471
+ };
472
+ }
473
+ }
474
+ return {
475
+ condition,
476
+ matched: false,
477
+ detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
478
+ };
479
+ }
480
+ case "stateSignatureChanges": {
481
+ if (!context.beforeSignature) {
482
+ return { condition, matched: false, detail: "No before-signature captured" };
483
+ }
484
+ const afterSignature = await captureStateSignature(page);
485
+ const matched = afterSignature !== context.beforeSignature;
486
+ return {
487
+ condition,
488
+ matched,
489
+ detail: matched ? "Page state changed" : "Page state unchanged"
490
+ };
491
+ }
492
+ default: {
493
+ const _exhaustive = condition;
494
+ return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
495
+ }
496
+ }
497
+ }
498
+ async function evaluateOutcome(page, options) {
499
+ const {
500
+ expectAny,
501
+ expectAll,
502
+ failIf,
503
+ dangerous = false,
504
+ networkTracker,
505
+ beforeSignature
506
+ } = options;
507
+ const allMatched = [];
508
+ const context = { networkTracker, beforeSignature };
509
+ if (failIf && failIf.length > 0) {
510
+ for (const condition of failIf) {
511
+ const result = await evaluateCondition(condition, page, context);
512
+ allMatched.push(result);
513
+ if (result.matched) {
514
+ return {
515
+ outcomeStatus: "failed",
516
+ matchedConditions: allMatched,
517
+ retrySafe: !dangerous
518
+ };
519
+ }
520
+ }
521
+ }
522
+ if (expectAll && expectAll.length > 0) {
523
+ let allPassed = true;
524
+ for (const condition of expectAll) {
525
+ const result = await evaluateCondition(condition, page, context);
526
+ allMatched.push(result);
527
+ if (!result.matched) {
528
+ allPassed = false;
529
+ }
530
+ }
531
+ if (!allPassed) {
532
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
533
+ return {
534
+ outcomeStatus: status,
535
+ matchedConditions: allMatched,
536
+ retrySafe: !dangerous
537
+ };
538
+ }
539
+ if (!expectAny || expectAny.length === 0) {
540
+ return {
541
+ outcomeStatus: "success",
542
+ matchedConditions: allMatched,
543
+ retrySafe: true
544
+ };
545
+ }
546
+ }
547
+ if (expectAny && expectAny.length > 0) {
548
+ let anyPassed = false;
549
+ for (const condition of expectAny) {
550
+ const result = await evaluateCondition(condition, page, context);
551
+ allMatched.push(result);
552
+ if (result.matched) {
553
+ anyPassed = true;
554
+ }
555
+ }
556
+ if (anyPassed) {
557
+ return {
558
+ outcomeStatus: "success",
559
+ matchedConditions: allMatched,
560
+ retrySafe: true
561
+ };
562
+ }
563
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
564
+ return {
565
+ outcomeStatus: status,
566
+ matchedConditions: allMatched,
567
+ retrySafe: !dangerous
568
+ };
569
+ }
570
+ return {
571
+ outcomeStatus: "success",
572
+ matchedConditions: allMatched,
573
+ retrySafe: true
574
+ };
575
+ }
576
+
577
+ // src/actions/combinators.ts
578
+ async function conditionAny(conditions, page, context) {
579
+ const results = [];
580
+ let winnerIndex;
581
+ for (let i = 0; i < conditions.length; i++) {
582
+ const result = await evaluateCondition(conditions[i], page, context);
583
+ results.push(result);
584
+ if (result.matched && winnerIndex === void 0) {
585
+ winnerIndex = i;
586
+ }
587
+ }
588
+ return {
589
+ matched: winnerIndex !== void 0,
590
+ matchedConditions: results,
591
+ winnerIndex
592
+ };
593
+ }
594
+ async function conditionAll(conditions, page, context) {
595
+ const results = [];
596
+ let allMatched = true;
597
+ for (const condition of conditions) {
598
+ const result = await evaluateCondition(condition, page, context);
599
+ results.push(result);
600
+ if (!result.matched) allMatched = false;
601
+ }
602
+ return {
603
+ matched: allMatched,
604
+ matchedConditions: results
605
+ };
606
+ }
607
+ async function conditionNot(condition, page, context) {
608
+ const result = await evaluateCondition(condition, page, context);
609
+ return {
610
+ matched: !result.matched,
611
+ matchedConditions: [
612
+ {
613
+ condition: result.condition,
614
+ matched: !result.matched,
615
+ detail: result.matched ? `NOT: condition was true (inverted to false): ${result.detail}` : `NOT: condition was false (inverted to true): ${result.detail}`
616
+ }
617
+ ]
618
+ };
619
+ }
620
+ async function conditionRace(conditions, page, options = {}) {
621
+ const { timeout = 1e4, pollInterval = 200, networkTracker, beforeSignature } = options;
622
+ const context = { networkTracker, beforeSignature };
623
+ const startTime = Date.now();
624
+ const deadline = startTime + timeout;
625
+ const immediate = await conditionAny(conditions, page, context);
626
+ if (immediate.matched) return immediate;
627
+ while (Date.now() < deadline) {
628
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
629
+ const result = await conditionAny(conditions, page, context);
630
+ if (result.matched) return result;
631
+ }
632
+ return await conditionAny(conditions, page, context);
633
+ }
634
+
75
635
  // src/actions/executor.ts
76
636
  var fs = __toESM(require("fs"), 1);
77
637
  var import_node_path = require("path");
@@ -942,7 +1502,9 @@ function buildTraceSummaries(events) {
942
1502
  };
943
1503
  }
944
1504
  function summarizeWs(events) {
945
- const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
1505
+ const relevant = events.filter(
1506
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
1507
+ );
946
1508
  const connections = /* @__PURE__ */ new Map();
947
1509
  for (const event of relevant) {
948
1510
  const id = event.connectionId ?? event.requestId ?? event.traceId;
@@ -972,7 +1534,7 @@ function summarizeWs(events) {
972
1534
  }
973
1535
  const values = [...connections.values()];
974
1536
  const reconnects = values.reduce((count, connection) => {
975
- return connection.closedAt && !connection.createdAt ? count : count;
1537
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
976
1538
  }, 0);
977
1539
  return {
978
1540
  view: "ws",
@@ -1225,6 +1787,31 @@ function frameToStep(frame) {
1225
1787
  }
1226
1788
  }
1227
1789
 
1790
+ // src/trace/model.ts
1791
+ function createTraceId(prefix = "evt") {
1792
+ const random = Math.random().toString(36).slice(2, 10);
1793
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1794
+ }
1795
+ function normalizeTraceEvent(event) {
1796
+ return {
1797
+ traceId: event.traceId ?? createTraceId(event.channel),
1798
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1799
+ elapsedMs: event.elapsedMs ?? 0,
1800
+ severity: event.severity ?? inferSeverity(event.event),
1801
+ data: event.data ?? {},
1802
+ ...event
1803
+ };
1804
+ }
1805
+ function inferSeverity(eventName) {
1806
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1807
+ return "error";
1808
+ }
1809
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1810
+ return "warn";
1811
+ }
1812
+ return "info";
1813
+ }
1814
+
1228
1815
  // src/trace/script.ts
1229
1816
  var TRACE_BINDING_NAME = "__bpTraceBinding";
1230
1817
  var TRACE_SCRIPT = `
@@ -1504,38 +2091,6 @@ var TRACE_SCRIPT = `
1504
2091
  })();
1505
2092
  `;
1506
2093
 
1507
- // src/trace/model.ts
1508
- function createTraceId(prefix = "evt") {
1509
- const random = Math.random().toString(36).slice(2, 10);
1510
- return `${prefix}-${Date.now().toString(36)}-${random}`;
1511
- }
1512
- function normalizeTraceEvent(event) {
1513
- return {
1514
- traceId: event.traceId ?? createTraceId(event.channel),
1515
- ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1516
- elapsedMs: event.elapsedMs ?? 0,
1517
- severity: event.severity ?? inferSeverity(event.event),
1518
- data: event.data ?? {},
1519
- ...event
1520
- };
1521
- }
1522
- function inferSeverity(eventName) {
1523
- if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1524
- return "error";
1525
- }
1526
- if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1527
- return "warn";
1528
- }
1529
- return "info";
1530
- }
1531
-
1532
- // src/trace/live.ts
1533
- function globToRegex(pattern) {
1534
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1535
- const withWildcards = escaped.replace(/\*/g, ".*");
1536
- return new RegExp(`^${withWildcards}$`);
1537
- }
1538
-
1539
2094
  // src/actions/executor.ts
1540
2095
  var DEFAULT_TIMEOUT = 3e4;
1541
2096
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -1658,6 +2213,25 @@ function getSuggestion(reason) {
1658
2213
  }
1659
2214
  }
1660
2215
  }
2216
+ function hasOutcomeConditions(step) {
2217
+ 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;
2218
+ }
2219
+ function needsNetworkTracking(step) {
2220
+ const allConditions = [
2221
+ ...step.expectAny ?? [],
2222
+ ...step.expectAll ?? [],
2223
+ ...step.failIf ?? []
2224
+ ];
2225
+ return allConditions.some((c) => c.kind === "networkResponse");
2226
+ }
2227
+ function needsStateSignature(step) {
2228
+ const allConditions = [
2229
+ ...step.expectAny ?? [],
2230
+ ...step.expectAll ?? [],
2231
+ ...step.failIf ?? []
2232
+ ];
2233
+ return allConditions.some((c) => c.kind === "stateSignatureChanges");
2234
+ }
1661
2235
  var BatchExecutor = class {
1662
2236
  page;
1663
2237
  constructor(page) {
@@ -1703,9 +2277,25 @@ var BatchExecutor = class {
1703
2277
  })
1704
2278
  );
1705
2279
  }
2280
+ const hasOutcome = hasOutcomeConditions(step);
2281
+ let networkTracker;
2282
+ let beforeSignature;
2283
+ if (hasOutcome) {
2284
+ if (needsNetworkTracking(step)) {
2285
+ networkTracker = new NetworkResponseTracker();
2286
+ networkTracker.start(this.page.cdpClient);
2287
+ }
2288
+ if (needsStateSignature(step)) {
2289
+ beforeSignature = await captureStateSignature(this.page);
2290
+ }
2291
+ }
1706
2292
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1707
2293
  if (attempt > 0) {
1708
2294
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
2295
+ if (networkTracker) networkTracker.reset();
2296
+ if (hasOutcome && needsStateSignature(step)) {
2297
+ beforeSignature = await captureStateSignature(this.page);
2298
+ }
1709
2299
  }
1710
2300
  try {
1711
2301
  this.page.resetLastActionPosition();
@@ -1723,6 +2313,28 @@ var BatchExecutor = class {
1723
2313
  coordinates: this.page.getLastActionCoordinates() ?? void 0,
1724
2314
  boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1725
2315
  };
2316
+ if (hasOutcome) {
2317
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
2318
+ const outcome = await evaluateOutcome(this.page, {
2319
+ expectAny: step.expectAny,
2320
+ expectAll: step.expectAll,
2321
+ failIf: step.failIf,
2322
+ dangerous: step.dangerous,
2323
+ networkTracker,
2324
+ beforeSignature
2325
+ });
2326
+ stepResult.outcomeStatus = outcome.outcomeStatus;
2327
+ stepResult.matchedConditions = outcome.matchedConditions;
2328
+ stepResult.retrySafe = outcome.retrySafe;
2329
+ if (outcome.outcomeStatus !== "success") {
2330
+ stepResult.success = false;
2331
+ stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
2332
+ const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
2333
+ if (failedDetails.length > 0) {
2334
+ stepResult.suggestion = failedDetails.join("; ");
2335
+ }
2336
+ }
2337
+ }
1726
2338
  if (recording && !recording.skipActions.has(step.action)) {
1727
2339
  await this.captureRecordingFrame(step, stepResult, recording);
1728
2340
  }
@@ -1732,13 +2344,14 @@ var BatchExecutor = class {
1732
2344
  traceId: createTraceId("action"),
1733
2345
  elapsedMs: Date.now() - startTime,
1734
2346
  channel: "action",
1735
- event: "action.succeeded",
1736
- summary: `${step.action} succeeded`,
2347
+ event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
2348
+ summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
1737
2349
  data: {
1738
2350
  action: step.action,
1739
2351
  selector: step.selector ?? null,
1740
2352
  selectorUsed: result.selectorUsed ?? null,
1741
- durationMs: Date.now() - stepStart
2353
+ durationMs: Date.now() - stepStart,
2354
+ outcomeStatus: stepResult.outcomeStatus ?? null
1742
2355
  },
1743
2356
  actionId: `action-${i + 1}`,
1744
2357
  stepIndex: i,
@@ -1748,6 +2361,18 @@ var BatchExecutor = class {
1748
2361
  })
1749
2362
  );
1750
2363
  }
2364
+ if (hasOutcome && !stepResult.success) {
2365
+ if (step.dangerous) {
2366
+ results.push(stepResult);
2367
+ break;
2368
+ }
2369
+ if (attempt < maxAttempts - 1) {
2370
+ lastError = new Error(stepResult.error ?? "Outcome failed");
2371
+ continue;
2372
+ }
2373
+ results.push(stepResult);
2374
+ break;
2375
+ }
1751
2376
  results.push(stepResult);
1752
2377
  succeeded = true;
1753
2378
  break;
@@ -1755,59 +2380,63 @@ var BatchExecutor = class {
1755
2380
  lastError = error instanceof Error ? error : new Error(String(error));
1756
2381
  }
1757
2382
  }
2383
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
1758
2384
  if (!succeeded) {
1759
- const errorMessage = lastError?.message ?? "Unknown error";
1760
- let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
1761
- const { reason, coveringElement } = classifyFailure(lastError);
1762
- if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
1763
- try {
1764
- const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
1765
- const autoHints = await generateHints(this.page, selectors, step.action, 3);
1766
- if (autoHints.length > 0) {
1767
- hints = autoHints;
2385
+ const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
2386
+ if (!resultAlreadyPushed) {
2387
+ const errorMessage = lastError?.message ?? "Unknown error";
2388
+ let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
2389
+ const { reason, coveringElement } = classifyFailure(lastError);
2390
+ if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
2391
+ try {
2392
+ const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
2393
+ const autoHints = await generateHints(this.page, selectors, step.action, 3);
2394
+ if (autoHints.length > 0) {
2395
+ hints = autoHints;
2396
+ }
2397
+ } catch {
1768
2398
  }
1769
- } catch {
1770
2399
  }
2400
+ const failedResult = {
2401
+ index: i,
2402
+ action: step.action,
2403
+ selector: step.selector,
2404
+ success: false,
2405
+ durationMs: Date.now() - stepStart,
2406
+ error: errorMessage,
2407
+ hints,
2408
+ failureReason: reason,
2409
+ coveringElement,
2410
+ suggestion: getSuggestion(reason),
2411
+ timestamp: Date.now()
2412
+ };
2413
+ if (recording && !recording.skipActions.has(step.action)) {
2414
+ await this.captureRecordingFrame(step, failedResult, recording);
2415
+ }
2416
+ if (recording) {
2417
+ recording.traceEvents.push(
2418
+ normalizeTraceEvent({
2419
+ traceId: createTraceId("action"),
2420
+ elapsedMs: Date.now() - startTime,
2421
+ channel: "action",
2422
+ event: "action.failed",
2423
+ severity: "error",
2424
+ summary: `${step.action} failed: ${errorMessage}`,
2425
+ data: {
2426
+ action: step.action,
2427
+ selector: step.selector ?? null,
2428
+ error: errorMessage,
2429
+ reason
2430
+ },
2431
+ actionId: `action-${i + 1}`,
2432
+ stepIndex: i,
2433
+ selector: step.selector,
2434
+ url: step.url
2435
+ })
2436
+ );
2437
+ }
2438
+ results.push(failedResult);
1771
2439
  }
1772
- const failedResult = {
1773
- index: i,
1774
- action: step.action,
1775
- selector: step.selector,
1776
- success: false,
1777
- durationMs: Date.now() - stepStart,
1778
- error: errorMessage,
1779
- hints,
1780
- failureReason: reason,
1781
- coveringElement,
1782
- suggestion: getSuggestion(reason),
1783
- timestamp: Date.now()
1784
- };
1785
- if (recording && !recording.skipActions.has(step.action)) {
1786
- await this.captureRecordingFrame(step, failedResult, recording);
1787
- }
1788
- if (recording) {
1789
- recording.traceEvents.push(
1790
- normalizeTraceEvent({
1791
- traceId: createTraceId("action"),
1792
- elapsedMs: Date.now() - startTime,
1793
- channel: "action",
1794
- event: "action.failed",
1795
- severity: "error",
1796
- summary: `${step.action} failed: ${errorMessage}`,
1797
- data: {
1798
- action: step.action,
1799
- selector: step.selector ?? null,
1800
- error: errorMessage,
1801
- reason
1802
- },
1803
- actionId: `action-${i + 1}`,
1804
- stepIndex: i,
1805
- selector: step.selector,
1806
- url: step.url
1807
- })
1808
- );
1809
- }
1810
- results.push(failedResult);
1811
2440
  if (onFail === "stop" && !step.optional) {
1812
2441
  stoppedAtIndex = i;
1813
2442
  break;
@@ -2118,6 +2747,14 @@ var BatchExecutor = class {
2118
2747
  case "forms": {
2119
2748
  return { value: await this.page.forms() };
2120
2749
  }
2750
+ case "delta": {
2751
+ const review = await this.page.review();
2752
+ return { value: review };
2753
+ }
2754
+ case "review": {
2755
+ const review = await this.page.review();
2756
+ return { value: review };
2757
+ }
2121
2758
  case "screenshot": {
2122
2759
  const data = await this.page.screenshot({
2123
2760
  format: step.format,
@@ -2268,6 +2905,35 @@ var BatchExecutor = class {
2268
2905
  const media = await this.assertMediaTrackLive(step.kind);
2269
2906
  return { value: media };
2270
2907
  }
2908
+ case "chooseOption": {
2909
+ const { chooseOption: chooseOption2 } = await Promise.resolve().then(() => (init_combobox(), combobox_exports));
2910
+ if (!step.value) throw new Error("chooseOption requires value");
2911
+ const result = await chooseOption2(this.page, {
2912
+ trigger: step.trigger ?? step.selector ?? "",
2913
+ listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
2914
+ value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
2915
+ match: step.match,
2916
+ timeout: step.timeout ?? timeout
2917
+ });
2918
+ if (!result.success) {
2919
+ throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
2920
+ }
2921
+ return { value: result };
2922
+ }
2923
+ case "upload": {
2924
+ const { uploadFiles: uploadFiles2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
2925
+ if (!step.selector) throw new Error("upload requires selector");
2926
+ if (!step.files || step.files.length === 0) throw new Error("upload requires files");
2927
+ const result = await uploadFiles2(this.page, {
2928
+ selector: step.selector,
2929
+ files: step.files,
2930
+ timeout: step.timeout ?? timeout
2931
+ });
2932
+ if (!result.accepted) {
2933
+ throw new Error(result.error ?? "Upload was not accepted");
2934
+ }
2935
+ return { value: result };
2936
+ }
2271
2937
  default: {
2272
2938
  const action = step.action;
2273
2939
  const aliases = {
@@ -2345,8 +3011,13 @@ Valid actions: ${valid}`);
2345
3011
  await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2346
3012
  } catch {
2347
3013
  }
2348
- await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
2349
- await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
3014
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
3015
+ source: TRACE_SCRIPT
3016
+ });
3017
+ await this.page.cdpClient.send("Runtime.evaluate", {
3018
+ expression: TRACE_SCRIPT,
3019
+ awaitPromise: false
3020
+ });
2350
3021
  }
2351
3022
  async waitForWsMessage(match, where, timeout) {
2352
3023
  await this.ensureTraceHooks();
@@ -2364,12 +3035,12 @@ Valid actions: ${valid}`);
2364
3035
  clearTimeout(timer);
2365
3036
  };
2366
3037
  const onCreated = (params) => {
2367
- wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
3038
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
2368
3039
  };
2369
3040
  const onFrame = (params) => {
2370
- const requestId = String(params["requestId"] ?? "");
3041
+ const requestId = readStringOr(params["requestId"]);
2371
3042
  const response = params["response"] ?? {};
2372
- const payload = String(response.payloadData ?? "");
3043
+ const payload = response.payloadData ?? "";
2373
3044
  const url = wsUrls.get(requestId) ?? "";
2374
3045
  if (!regex.test(url) && !regex.test(payload)) {
2375
3046
  return;
@@ -2385,13 +3056,13 @@ Valid actions: ${valid}`);
2385
3056
  return;
2386
3057
  }
2387
3058
  try {
2388
- const parsed = JSON.parse(String(params["payload"] ?? ""));
3059
+ const parsed = JSON.parse(readStringOr(params["payload"]));
2389
3060
  if (parsed.event !== "ws.frame.received") {
2390
3061
  return;
2391
3062
  }
2392
3063
  const data = parsed.data ?? {};
2393
- const payload = String(data["payload"] ?? "");
2394
- const url = String(data["url"] ?? "");
3064
+ const payload = readStringOr(data["payload"]);
3065
+ const url = readStringOr(data["url"]);
2395
3066
  if (!regex.test(url) && !regex.test(payload)) {
2396
3067
  return;
2397
3068
  }
@@ -2400,7 +3071,7 @@ Valid actions: ${valid}`);
2400
3071
  }
2401
3072
  cleanup();
2402
3073
  resolve({
2403
- requestId: String(data["connectionId"] ?? ""),
3074
+ requestId: readStringOr(data["connectionId"]),
2404
3075
  url,
2405
3076
  payload
2406
3077
  });
@@ -2444,13 +3115,14 @@ Valid actions: ${valid}`);
2444
3115
  if (!entry || typeof entry !== "object") {
2445
3116
  continue;
2446
3117
  }
2447
- const event = String(entry["event"] ?? "");
3118
+ const record = entry;
3119
+ const event = readStringOr(record["event"]);
2448
3120
  if (event !== "ws.frame.received") {
2449
3121
  continue;
2450
3122
  }
2451
- const data = entry["data"] ?? {};
2452
- const payload = String(data["payload"] ?? "");
2453
- const url = String(data["url"] ?? "");
3123
+ const data = record["data"] ?? {};
3124
+ const payload = readStringOr(data["payload"]);
3125
+ const url = readStringOr(data["url"]);
2454
3126
  if (!regex.test(url) && !regex.test(payload)) {
2455
3127
  continue;
2456
3128
  }
@@ -2458,7 +3130,7 @@ Valid actions: ${valid}`);
2458
3130
  continue;
2459
3131
  }
2460
3132
  return {
2461
- requestId: String(data["connectionId"] ?? ""),
3133
+ requestId: readStringOr(data["connectionId"]),
2462
3134
  url,
2463
3135
  payload
2464
3136
  };
@@ -2479,13 +3151,11 @@ Valid actions: ${valid}`);
2479
3151
  return;
2480
3152
  }
2481
3153
  const args = Array.isArray(params["args"]) ? params["args"] : [];
2482
- errors.push(
2483
- args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
2484
- );
3154
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
2485
3155
  };
2486
3156
  const onException = (params) => {
2487
3157
  const details = params["exceptionDetails"] ?? {};
2488
- errors.push(String(details["text"] ?? "Runtime exception"));
3158
+ errors.push(readString(details["text"]) ?? "Runtime exception");
2489
3159
  };
2490
3160
  const timer = setTimeout(() => {
2491
3161
  cleanup();
@@ -2857,6 +3527,30 @@ var ACTION_RULES = {
2857
3527
  kind: { type: "string", enum: ["audio", "video"] }
2858
3528
  },
2859
3529
  optional: {}
3530
+ },
3531
+ delta: {
3532
+ required: {},
3533
+ optional: {}
3534
+ },
3535
+ review: {
3536
+ required: {},
3537
+ optional: {}
3538
+ },
3539
+ chooseOption: {
3540
+ required: { value: { type: "string|string[]" } },
3541
+ optional: {
3542
+ trigger: { type: "string|string[]" },
3543
+ selector: { type: "string|string[]" },
3544
+ option: { type: "string|string[]" },
3545
+ match: { type: "string", enum: ["exact", "contains", "startsWith"] }
3546
+ }
3547
+ },
3548
+ upload: {
3549
+ required: {
3550
+ selector: { type: "string|string[]" },
3551
+ files: { type: "string|string[]" }
3552
+ },
3553
+ optional: {}
2860
3554
  }
2861
3555
  };
2862
3556
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -2896,7 +3590,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
2896
3590
  "name",
2897
3591
  "state",
2898
3592
  "kind",
2899
- "windowMs"
3593
+ "windowMs",
3594
+ "expectAny",
3595
+ "expectAll",
3596
+ "failIf",
3597
+ "dangerous",
3598
+ "files"
2900
3599
  ]);
2901
3600
  function resolveAction(name) {
2902
3601
  if (VALID_ACTIONS.includes(name)) {
@@ -3116,6 +3815,64 @@ function validateSteps(steps) {
3116
3815
  });
3117
3816
  }
3118
3817
  }
3818
+ if ("dangerous" in obj && obj["dangerous"] !== void 0) {
3819
+ if (typeof obj["dangerous"] !== "boolean") {
3820
+ errors.push({
3821
+ stepIndex: i,
3822
+ field: "dangerous",
3823
+ message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
3824
+ });
3825
+ }
3826
+ }
3827
+ for (const condField of ["expectAny", "expectAll", "failIf"]) {
3828
+ if (condField in obj && obj[condField] !== void 0) {
3829
+ if (!Array.isArray(obj[condField])) {
3830
+ errors.push({
3831
+ stepIndex: i,
3832
+ field: condField,
3833
+ message: `"${condField}" expected array, got ${typeof obj[condField]}.`
3834
+ });
3835
+ } else {
3836
+ const conditions = obj[condField];
3837
+ for (let ci = 0; ci < conditions.length; ci++) {
3838
+ const cond = conditions[ci];
3839
+ if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
3840
+ errors.push({
3841
+ stepIndex: i,
3842
+ field: condField,
3843
+ message: `"${condField}[${ci}]" must be a condition object.`
3844
+ });
3845
+ continue;
3846
+ }
3847
+ const condObj = cond;
3848
+ if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
3849
+ errors.push({
3850
+ stepIndex: i,
3851
+ field: condField,
3852
+ message: `"${condField}[${ci}]" missing required "kind" field.`
3853
+ });
3854
+ } else {
3855
+ const validKinds = [
3856
+ "urlMatches",
3857
+ "elementVisible",
3858
+ "elementHidden",
3859
+ "textAppears",
3860
+ "textChanges",
3861
+ "networkResponse",
3862
+ "stateSignatureChanges"
3863
+ ];
3864
+ if (!validKinds.includes(condObj["kind"])) {
3865
+ errors.push({
3866
+ stepIndex: i,
3867
+ field: condField,
3868
+ message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
3869
+ });
3870
+ }
3871
+ }
3872
+ }
3873
+ }
3874
+ }
3875
+ }
3119
3876
  if (action === "assertText") {
3120
3877
  if (!("expect" in obj) && !("value" in obj)) {
3121
3878
  errors.push({
@@ -3258,12 +4015,12 @@ function parseWavHeader(data) {
3258
4015
  if (data.byteLength < 44) {
3259
4016
  throw new Error("Invalid WAV: file too small");
3260
4017
  }
3261
- const riff = readString(view, 0, 4);
3262
- const wave = readString(view, 8, 4);
4018
+ const riff = readString2(view, 0, 4);
4019
+ const wave = readString2(view, 8, 4);
3263
4020
  if (riff !== "RIFF" || wave !== "WAVE") {
3264
4021
  throw new Error("Invalid WAV: missing RIFF/WAVE header");
3265
4022
  }
3266
- const fmt = readString(view, 12, 4);
4023
+ const fmt = readString2(view, 12, 4);
3267
4024
  if (fmt !== "fmt ") {
3268
4025
  throw new Error("Invalid WAV: missing fmt chunk");
3269
4026
  }
@@ -3272,7 +4029,7 @@ function parseWavHeader(data) {
3272
4029
  const bitsPerSample = view.getUint16(34, true);
3273
4030
  let dataOffset = 36;
3274
4031
  while (dataOffset < data.byteLength - 8) {
3275
- const chunkId = readString(view, dataOffset, 4);
4032
+ const chunkId = readString2(view, dataOffset, 4);
3276
4033
  const chunkSize = view.getUint32(dataOffset + 4, true);
3277
4034
  if (chunkId === "data") {
3278
4035
  return {
@@ -3303,7 +4060,7 @@ function writeString(view, offset, str) {
3303
4060
  view.setUint8(offset + i, str.charCodeAt(i));
3304
4061
  }
3305
4062
  }
3306
- function readString(view, offset, length) {
4063
+ function readString2(view, offset, length) {
3307
4064
  let str = "";
3308
4065
  for (let i = 0; i < length; i++) {
3309
4066
  str += String.fromCharCode(view.getUint8(offset + i));
@@ -5056,40 +5813,364 @@ async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
5056
5813
  `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
5057
5814
  );
5058
5815
  }
5059
- if (attempt < attempts) {
5060
- await sleep3(delayMs);
5061
- delayMs = Math.min(delayMs * 2, maxDelayMs);
5816
+ if (attempt < attempts) {
5817
+ await sleep3(delayMs);
5818
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
5819
+ }
5820
+ }
5821
+ throw lastError ?? new Error(errorPrefix);
5822
+ }
5823
+ var GenericProvider = class {
5824
+ name = "generic";
5825
+ wsUrl;
5826
+ constructor(options) {
5827
+ this.wsUrl = options.wsUrl;
5828
+ }
5829
+ async createSession(_options = {}) {
5830
+ return {
5831
+ wsUrl: this.wsUrl,
5832
+ metadata: {
5833
+ provider: "generic"
5834
+ },
5835
+ close: async () => {
5836
+ }
5837
+ };
5838
+ }
5839
+ };
5840
+ async function discoverTargets(host = "localhost:9222") {
5841
+ return fetchDevToolsJson(host, "/json/list", "Failed to discover targets");
5842
+ }
5843
+ async function getBrowserWebSocketUrl(host = "localhost:9222") {
5844
+ const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
5845
+ attempts: 10,
5846
+ initialDelayMs: 50,
5847
+ maxDelayMs: 250
5848
+ });
5849
+ return info.webSocketDebuggerUrl;
5850
+ }
5851
+
5852
+ // src/providers/local-discovery.ts
5853
+ var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
5854
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
5855
+ var DevToolsActivePortParseError = class extends Error {
5856
+ constructor(message, reason) {
5857
+ super(message);
5858
+ this.reason = reason;
5859
+ this.name = "DevToolsActivePortParseError";
5860
+ }
5861
+ };
5862
+ function getRuntimeEnv() {
5863
+ if (typeof process === "undefined") {
5864
+ return {};
5865
+ }
5866
+ return process.env;
5867
+ }
5868
+ function getRuntimePlatform() {
5869
+ if (typeof process === "undefined") {
5870
+ return void 0;
5871
+ }
5872
+ return process.platform;
5873
+ }
5874
+ function normalizePlatform(platform) {
5875
+ if (platform === "darwin" || platform === "linux" || platform === "win32") {
5876
+ return platform;
5877
+ }
5878
+ throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
5879
+ }
5880
+ function trimTrailingSeparator(path) {
5881
+ return path.replace(/[\\/]+$/, "");
5882
+ }
5883
+ function joinPath(platform, ...parts) {
5884
+ const separator = platform === "win32" ? "\\" : "/";
5885
+ const cleaned = parts.map((part, index) => {
5886
+ if (index === 0) return trimTrailingSeparator(part);
5887
+ return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
5888
+ }).filter((part) => part.length > 0);
5889
+ return cleaned.join(separator);
5890
+ }
5891
+ function resolveHomeDir(platform, env, explicitHomeDir) {
5892
+ if (explicitHomeDir) {
5893
+ return explicitHomeDir;
5894
+ }
5895
+ if (platform === "win32") {
5896
+ return env["USERPROFILE"] ?? env["HOME"] ?? "";
5897
+ }
5898
+ return env["HOME"] ?? env["USERPROFILE"] ?? "";
5899
+ }
5900
+ function toFileFailure(target, error) {
5901
+ const errno = error?.code;
5902
+ if (errno === "ENOENT") {
5903
+ return {
5904
+ ...target,
5905
+ reason: "missing-file",
5906
+ message: `DevToolsActivePort not found at ${target.portFile}`
5907
+ };
5908
+ }
5909
+ return {
5910
+ ...target,
5911
+ reason: "unreadable-file",
5912
+ message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
5913
+ };
5914
+ }
5915
+ function toProbeFailure(target, wsUrl, error) {
5916
+ const message = error instanceof Error ? error.message : String(error);
5917
+ const lowerMessage = message.toLowerCase();
5918
+ let reason = "connection-error";
5919
+ if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
5920
+ reason = "connection-refused";
5921
+ } else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
5922
+ reason = "connection-timeout";
5923
+ } else if (lowerMessage.includes("closed")) {
5924
+ reason = "unexpected-close";
5925
+ } else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
5926
+ reason = "cdp-error";
5927
+ }
5928
+ return {
5929
+ ...target,
5930
+ wsUrl,
5931
+ reason,
5932
+ message
5933
+ };
5934
+ }
5935
+ async function readTextFile(path) {
5936
+ const fs2 = await import("fs/promises");
5937
+ return fs2.readFile(path, "utf-8");
5938
+ }
5939
+ async function probeBrowserWebSocket(wsUrl, timeoutMs) {
5940
+ let client;
5941
+ try {
5942
+ client = await createCDPClient(wsUrl, { timeout: timeoutMs });
5943
+ const version = await client.send("Browser.getVersion", void 0, null);
5944
+ return { browserVersion: version.product };
5945
+ } finally {
5946
+ await client?.close().catch(() => {
5947
+ });
5948
+ }
5949
+ }
5950
+ var defaultDependencies = {
5951
+ readTextFile,
5952
+ probeBrowserWebSocket,
5953
+ getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
5954
+ };
5955
+ function resolveChromeUserDataDirs(options = {}) {
5956
+ const env = options.env ?? getRuntimeEnv();
5957
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
5958
+ const homeDir = resolveHomeDir(platform, env, options.homeDir);
5959
+ if (!homeDir) {
5960
+ throw new Error("Could not determine home directory for local Chrome discovery");
5961
+ }
5962
+ switch (platform) {
5963
+ case "darwin": {
5964
+ const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
5965
+ return {
5966
+ stable: joinPath(platform, base, "Chrome"),
5967
+ beta: joinPath(platform, base, "Chrome Beta"),
5968
+ dev: joinPath(platform, base, "Chrome Dev"),
5969
+ canary: joinPath(platform, base, "Chrome Canary")
5970
+ };
5971
+ }
5972
+ case "linux": {
5973
+ const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
5974
+ return {
5975
+ stable: joinPath(platform, configHome, "google-chrome"),
5976
+ beta: joinPath(platform, configHome, "google-chrome-beta"),
5977
+ dev: joinPath(platform, configHome, "google-chrome-dev"),
5978
+ canary: joinPath(platform, configHome, "google-chrome-canary")
5979
+ };
5980
+ }
5981
+ case "win32": {
5982
+ const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
5983
+ const base = joinPath(platform, localAppData, "Google");
5984
+ return {
5985
+ stable: joinPath(platform, base, "Chrome", "User Data"),
5986
+ beta: joinPath(platform, base, "Chrome Beta", "User Data"),
5987
+ dev: joinPath(platform, base, "Chrome Dev", "User Data"),
5988
+ canary: joinPath(platform, base, "Chrome SxS", "User Data")
5989
+ };
5990
+ }
5991
+ }
5992
+ throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
5993
+ }
5994
+ function buildLocalBrowserScanTargets(options = {}) {
5995
+ const env = options.env ?? getRuntimeEnv();
5996
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
5997
+ if (options.userDataDir) {
5998
+ return [
5999
+ {
6000
+ channel: options.channel ?? "custom",
6001
+ userDataDir: options.userDataDir,
6002
+ portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
6003
+ }
6004
+ ];
6005
+ }
6006
+ const dirs = resolveChromeUserDataDirs({
6007
+ platform,
6008
+ env,
6009
+ homeDir: options.homeDir
6010
+ });
6011
+ const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
6012
+ return channels.map((channel) => ({
6013
+ channel,
6014
+ userDataDir: dirs[channel],
6015
+ portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
6016
+ }));
6017
+ }
6018
+ function parseDevToolsActivePortFile(content) {
6019
+ const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
6020
+ if (lines.length !== 2) {
6021
+ throw new DevToolsActivePortParseError(
6022
+ `Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
6023
+ "malformed-file"
6024
+ );
6025
+ }
6026
+ const portText = lines[0];
6027
+ const browserPath = lines[1];
6028
+ const port = Number.parseInt(portText, 10);
6029
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
6030
+ throw new DevToolsActivePortParseError(
6031
+ `Invalid DevToolsActivePort port: ${portText}`,
6032
+ "invalid-port"
6033
+ );
6034
+ }
6035
+ if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
6036
+ throw new DevToolsActivePortParseError(
6037
+ `Invalid DevToolsActivePort browser path: ${browserPath}`,
6038
+ "invalid-path"
6039
+ );
6040
+ }
6041
+ return {
6042
+ port,
6043
+ browserPath,
6044
+ wsUrl: `ws://127.0.0.1:${port}${browserPath}`
6045
+ };
6046
+ }
6047
+ async function inspectScanTarget(target, options, deps) {
6048
+ let content;
6049
+ try {
6050
+ content = await deps.readTextFile(target.portFile);
6051
+ } catch (error) {
6052
+ return { kind: "failure", failure: toFileFailure(target, error) };
6053
+ }
6054
+ let parsed;
6055
+ try {
6056
+ parsed = parseDevToolsActivePortFile(content);
6057
+ } catch (error) {
6058
+ if (error instanceof DevToolsActivePortParseError) {
6059
+ return {
6060
+ kind: "failure",
6061
+ failure: {
6062
+ ...target,
6063
+ reason: error.reason,
6064
+ message: error.message
6065
+ }
6066
+ };
6067
+ }
6068
+ throw error;
6069
+ }
6070
+ try {
6071
+ const probe = await deps.probeBrowserWebSocket(
6072
+ parsed.wsUrl,
6073
+ options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
6074
+ );
6075
+ return {
6076
+ kind: "candidate",
6077
+ candidate: {
6078
+ ...target,
6079
+ port: parsed.port,
6080
+ browserPath: parsed.browserPath,
6081
+ wsUrl: parsed.wsUrl,
6082
+ browserVersion: probe.browserVersion
6083
+ }
6084
+ };
6085
+ } catch (error) {
6086
+ return {
6087
+ kind: "failure",
6088
+ failure: toProbeFailure(target, parsed.wsUrl, error)
6089
+ };
6090
+ }
6091
+ }
6092
+ async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
6093
+ const scanTargets = buildLocalBrowserScanTargets(options);
6094
+ const outcomes = await Promise.all(
6095
+ scanTargets.map((target) => inspectScanTarget(target, options, deps))
6096
+ );
6097
+ const candidates = [];
6098
+ const failures = [];
6099
+ for (const outcome of outcomes) {
6100
+ if (outcome.kind === "candidate") {
6101
+ candidates.push(outcome.candidate);
6102
+ } else {
6103
+ failures.push(outcome.failure);
5062
6104
  }
5063
6105
  }
5064
- throw lastError ?? new Error(errorPrefix);
6106
+ return { candidates, failures };
5065
6107
  }
5066
- var GenericProvider = class {
5067
- name = "generic";
5068
- wsUrl;
5069
- constructor(options) {
5070
- this.wsUrl = options.wsUrl;
6108
+ var BrowserEndpointResolutionError = class extends Error {
6109
+ constructor(code, message, details = {}) {
6110
+ super(message);
6111
+ this.code = code;
6112
+ this.details = details;
5071
6113
  }
5072
- async createSession(_options = {}) {
6114
+ name = "BrowserEndpointResolutionError";
6115
+ };
6116
+ async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
6117
+ if (options.explicitWsUrl) {
5073
6118
  return {
5074
- wsUrl: this.wsUrl,
5075
- metadata: {
5076
- provider: "generic"
5077
- },
5078
- close: async () => {
5079
- }
6119
+ wsUrl: options.explicitWsUrl,
6120
+ source: "explicit-ws"
5080
6121
  };
5081
6122
  }
5082
- };
5083
- async function discoverTargets(host = "localhost:9222") {
5084
- return fetchDevToolsJson(host, "/json/list", "Failed to discover targets");
5085
- }
5086
- async function getBrowserWebSocketUrl(host = "localhost:9222") {
5087
- const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
5088
- attempts: 10,
5089
- initialDelayMs: 50,
5090
- maxDelayMs: 250
5091
- });
5092
- return info.webSocketDebuggerUrl;
6123
+ let localDiscovery;
6124
+ if (options.allowLocalDiscovery ?? true) {
6125
+ localDiscovery = await discoverLocalBrowsers(options, deps);
6126
+ if (localDiscovery.candidates.length === 1) {
6127
+ const candidate = localDiscovery.candidates[0];
6128
+ return {
6129
+ wsUrl: candidate.wsUrl,
6130
+ source: "devtools-active-port",
6131
+ channel: candidate.channel,
6132
+ userDataDir: candidate.userDataDir
6133
+ };
6134
+ }
6135
+ if (localDiscovery.candidates.length > 1) {
6136
+ throw new BrowserEndpointResolutionError(
6137
+ "multiple-local-browsers",
6138
+ "Multiple local Chrome profiles are available for auto-discovery",
6139
+ {
6140
+ candidates: localDiscovery.candidates,
6141
+ failures: localDiscovery.failures
6142
+ }
6143
+ );
6144
+ }
6145
+ }
6146
+ if (options.allowLegacyHostFallback ?? true) {
6147
+ const legacyHost = options.legacyHost ?? "localhost:9222";
6148
+ try {
6149
+ return {
6150
+ wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
6151
+ source: "json-version"
6152
+ };
6153
+ } catch (error) {
6154
+ throw new BrowserEndpointResolutionError(
6155
+ "browser-not-found",
6156
+ "Could not resolve a browser endpoint",
6157
+ {
6158
+ candidates: localDiscovery?.candidates,
6159
+ failures: localDiscovery?.failures,
6160
+ legacyError: error instanceof Error ? error : new Error(String(error)),
6161
+ legacyHost
6162
+ }
6163
+ );
6164
+ }
6165
+ }
6166
+ throw new BrowserEndpointResolutionError(
6167
+ "browser-not-found",
6168
+ "Could not resolve a browser endpoint",
6169
+ {
6170
+ candidates: localDiscovery?.candidates,
6171
+ failures: localDiscovery?.failures
6172
+ }
6173
+ );
5093
6174
  }
5094
6175
 
5095
6176
  // src/providers/index.ts
@@ -5892,6 +6973,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
5892
6973
  });
5893
6974
  }
5894
6975
 
6976
+ // src/browser/delta.ts
6977
+ function extractPageState(url, title, snapshot, forms, pageText) {
6978
+ const headings = [];
6979
+ const buttons = [];
6980
+ const alerts = [];
6981
+ function walkNodes(nodes) {
6982
+ for (const node of nodes) {
6983
+ const role = node.role?.toLowerCase() ?? "";
6984
+ if (role === "heading" && node.name) {
6985
+ headings.push(node.name);
6986
+ }
6987
+ if ((role === "button" || role === "link") && node.name) {
6988
+ const disabled = node.disabled ?? false;
6989
+ buttons.push({ text: node.name, disabled, ref: node.ref });
6990
+ }
6991
+ if (role === "alert" && node.name) {
6992
+ alerts.push(node.name);
6993
+ }
6994
+ if (node.children) {
6995
+ walkNodes(node.children);
6996
+ }
6997
+ }
6998
+ }
6999
+ walkNodes(snapshot.accessibilityTree);
7000
+ const formFields = forms.map((f) => ({
7001
+ label: f.label,
7002
+ name: f.name,
7003
+ id: f.id,
7004
+ value: f.value,
7005
+ type: f.type
7006
+ }));
7007
+ return {
7008
+ url,
7009
+ title,
7010
+ headings,
7011
+ formFields,
7012
+ buttons,
7013
+ alerts,
7014
+ visibleText: pageText.slice(0, 3e3)
7015
+ };
7016
+ }
7017
+ function computeDelta(before, after) {
7018
+ const changes = [];
7019
+ if (before.url !== after.url) {
7020
+ changes.push({ kind: "url", before: before.url, after: after.url });
7021
+ }
7022
+ if (before.title !== after.title) {
7023
+ changes.push({ kind: "title", before: before.title, after: after.title });
7024
+ }
7025
+ const beforeHeadings = new Set(before.headings);
7026
+ const afterHeadings = new Set(after.headings);
7027
+ for (const h of after.headings) {
7028
+ if (!beforeHeadings.has(h)) {
7029
+ changes.push({ kind: "heading_added", after: h });
7030
+ }
7031
+ }
7032
+ for (const h of before.headings) {
7033
+ if (!afterHeadings.has(h)) {
7034
+ changes.push({ kind: "heading_removed", before: h });
7035
+ }
7036
+ }
7037
+ const beforeFieldMap = new Map(
7038
+ before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
7039
+ );
7040
+ for (const af of after.formFields) {
7041
+ const key = af.id ?? af.name ?? af.label ?? "";
7042
+ const bf = beforeFieldMap.get(key);
7043
+ if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
7044
+ changes.push({
7045
+ kind: "field_changed",
7046
+ before: String(bf.value ?? ""),
7047
+ after: String(af.value ?? ""),
7048
+ detail: af.label ?? af.name ?? af.id ?? key
7049
+ });
7050
+ }
7051
+ }
7052
+ const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
7053
+ for (const ab of after.buttons) {
7054
+ const bb = beforeBtnMap.get(ab.text);
7055
+ if (bb && bb.disabled !== ab.disabled) {
7056
+ changes.push({
7057
+ kind: "button_changed",
7058
+ detail: ab.text,
7059
+ before: bb.disabled ? "disabled" : "enabled",
7060
+ after: ab.disabled ? "disabled" : "enabled"
7061
+ });
7062
+ }
7063
+ }
7064
+ const beforeAlerts = new Set(before.alerts);
7065
+ const afterAlerts = new Set(after.alerts);
7066
+ for (const a of after.alerts) {
7067
+ if (!beforeAlerts.has(a)) {
7068
+ changes.push({ kind: "alert_added", after: a });
7069
+ }
7070
+ }
7071
+ for (const a of before.alerts) {
7072
+ if (!afterAlerts.has(a)) {
7073
+ changes.push({ kind: "alert_removed", before: a });
7074
+ }
7075
+ }
7076
+ return {
7077
+ changes,
7078
+ before,
7079
+ after,
7080
+ hasChanges: changes.length > 0
7081
+ };
7082
+ }
7083
+
5895
7084
  // src/browser/keyboard.ts
5896
7085
  var US_KEYBOARD = {
5897
7086
  // Letters (lowercase)
@@ -6051,8 +7240,118 @@ function parseShortcut(combo) {
6051
7240
  return { modifiers, key };
6052
7241
  }
6053
7242
 
7243
+ // src/browser/review.ts
7244
+ function extractReview(url, title, snapshot, forms, pageText) {
7245
+ const headings = [];
7246
+ const alerts = [];
7247
+ const statusLabels = [];
7248
+ const keyValues = [];
7249
+ const tables = [];
7250
+ const summaryCards = [];
7251
+ function walkNodes(nodes, parentHeading) {
7252
+ let currentHeading = parentHeading;
7253
+ for (const node of nodes) {
7254
+ const role = node.role?.toLowerCase() ?? "";
7255
+ if (role === "heading" && node.name) {
7256
+ headings.push(node.name);
7257
+ currentHeading = node.name;
7258
+ }
7259
+ if (role === "alert" && node.name) {
7260
+ alerts.push(node.name);
7261
+ }
7262
+ if (role === "status" && node.name) {
7263
+ statusLabels.push(node.name);
7264
+ }
7265
+ if (role === "table" || role === "grid") {
7266
+ const table = extractTableFromNode(node);
7267
+ if (table) tables.push(table);
7268
+ }
7269
+ if ((role === "definition" || role === "term") && node.name) {
7270
+ if (role === "term") {
7271
+ keyValues.push({ key: node.name, value: "" });
7272
+ } else if (role === "definition" && keyValues.length > 0) {
7273
+ const last = keyValues[keyValues.length - 1];
7274
+ if (!last.value) last.value = node.name;
7275
+ }
7276
+ }
7277
+ if (node.children) {
7278
+ walkNodes(node.children, currentHeading);
7279
+ }
7280
+ }
7281
+ }
7282
+ walkNodes(snapshot.accessibilityTree);
7283
+ const textKvPairs = extractKeyValueFromText(pageText);
7284
+ keyValues.push(...textKvPairs);
7285
+ const formEntries = forms.map((f) => ({
7286
+ label: f.label,
7287
+ value: f.value,
7288
+ type: f.type,
7289
+ disabled: f.disabled
7290
+ }));
7291
+ return {
7292
+ url,
7293
+ title,
7294
+ headings,
7295
+ forms: formEntries,
7296
+ alerts,
7297
+ summaryCards,
7298
+ tables,
7299
+ keyValues,
7300
+ statusLabels
7301
+ };
7302
+ }
7303
+ function extractTableFromNode(node) {
7304
+ const headers = [];
7305
+ const rows = [];
7306
+ function findRows(n) {
7307
+ const role = n.role?.toLowerCase() ?? "";
7308
+ if (role === "columnheader" && n.name) {
7309
+ headers.push(n.name);
7310
+ }
7311
+ if (role === "row") {
7312
+ const cells = [];
7313
+ if (n.children) {
7314
+ for (const child of n.children) {
7315
+ const childRole = child.role?.toLowerCase() ?? "";
7316
+ if ((childRole === "cell" || childRole === "gridcell") && child.name) {
7317
+ cells.push(child.name);
7318
+ }
7319
+ }
7320
+ }
7321
+ if (cells.length > 0) rows.push(cells);
7322
+ }
7323
+ if (n.children) {
7324
+ for (const child of n.children) findRows(child);
7325
+ }
7326
+ }
7327
+ findRows(node);
7328
+ if (rows.length === 0) return null;
7329
+ return { headers, rows };
7330
+ }
7331
+ function extractKeyValueFromText(text) {
7332
+ const pairs = [];
7333
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
7334
+ for (const line of lines) {
7335
+ const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
7336
+ if (match) {
7337
+ pairs.push({ key: match[1].trim(), value: match[2].trim() });
7338
+ }
7339
+ }
7340
+ return pairs.slice(0, 20);
7341
+ }
7342
+
6054
7343
  // src/browser/page.ts
6055
7344
  var DEFAULT_TIMEOUT2 = 3e4;
7345
+ function normalizeAXCheckedValue(value) {
7346
+ if (typeof value === "boolean") {
7347
+ return value;
7348
+ }
7349
+ if (typeof value === "string") {
7350
+ if (value === "true") return true;
7351
+ if (value === "false") return false;
7352
+ }
7353
+ return void 0;
7354
+ }
6056
7355
  var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
6057
7356
  if (globalThis.__bpEventListenerTrackerInstalled) return;
6058
7357
  Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
@@ -7838,7 +9137,9 @@ var Page = class {
7838
9137
  }
7839
9138
  }
7840
9139
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7841
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
9140
+ const checked = normalizeAXCheckedValue(
9141
+ node.properties?.find((p) => p.name === "checked")?.value.value
9142
+ );
7842
9143
  return {
7843
9144
  role,
7844
9145
  name,
@@ -7894,7 +9195,9 @@ var Page = class {
7894
9195
  const ref = nodeRefs.get(node.nodeId);
7895
9196
  const name = node.name?.value ?? "";
7896
9197
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7897
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
9198
+ const checked = normalizeAXCheckedValue(
9199
+ node.properties?.find((p) => p.name === "checked")?.value.value
9200
+ );
7898
9201
  const value = node.value?.value;
7899
9202
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
7900
9203
  interactiveElements.push({
@@ -7961,6 +9264,45 @@ var Page = class {
7961
9264
  }
7962
9265
  }
7963
9266
  }
9267
+ // ============ Delta & Review ============
9268
+ /**
9269
+ * Capture current page state for delta comparison.
9270
+ * Call before an action, then call delta() again after and use computeDelta().
9271
+ */
9272
+ async captureState() {
9273
+ const [url, title, snapshot, forms, text] = await Promise.all([
9274
+ this.url(),
9275
+ this.title(),
9276
+ this.snapshot(),
9277
+ this.forms(),
9278
+ this.text()
9279
+ ]);
9280
+ return extractPageState(url, title, snapshot, forms, text);
9281
+ }
9282
+ /**
9283
+ * Compute what changed between two page states.
9284
+ * If no arguments: captures current state and returns it (for use as "before").
9285
+ * If one argument (before state): captures current state and computes delta.
9286
+ */
9287
+ async delta(before) {
9288
+ const currentState = await this.captureState();
9289
+ if (!before) return currentState;
9290
+ return computeDelta(before, currentState);
9291
+ }
9292
+ /**
9293
+ * Extract structured review surface from the current page.
9294
+ * Returns headings, form values, alerts, key-value pairs, tables, and status labels.
9295
+ */
9296
+ async review() {
9297
+ const [url, title, snapshot, forms, text] = await Promise.all([
9298
+ this.url(),
9299
+ this.title(),
9300
+ this.snapshot(),
9301
+ this.forms(),
9302
+ this.text()
9303
+ ]);
9304
+ return extractReview(url, title, snapshot, forms, text);
9305
+ }
7964
9306
  // ============ Batch Execution ============
7965
9307
  /**
7966
9308
  * Execute a batch of steps
@@ -9186,13 +10528,26 @@ var Browser = class _Browser {
9186
10528
  * Connect to a browser instance
9187
10529
  */
9188
10530
  static async connect(options) {
9189
- const provider = createProvider(options);
9190
- const session = await provider.createSession(options.session);
10531
+ let connectOptions = options;
10532
+ if (options.provider === "generic" && !options.wsUrl) {
10533
+ const endpoint = await resolveBrowserEndpoint({
10534
+ channel: options.channel,
10535
+ userDataDir: options.userDataDir,
10536
+ allowLocalDiscovery: true,
10537
+ allowLegacyHostFallback: true
10538
+ });
10539
+ connectOptions = {
10540
+ ...options,
10541
+ wsUrl: endpoint.wsUrl
10542
+ };
10543
+ }
10544
+ const provider = createProvider(connectOptions);
10545
+ const session = await provider.createSession(connectOptions.session);
9191
10546
  const cdp = await createCDPClient(session.wsUrl, {
9192
- debug: options.debug,
9193
- timeout: options.timeout
10547
+ debug: connectOptions.debug,
10548
+ timeout: connectOptions.timeout
9194
10549
  });
9195
- return new _Browser(cdp, provider, session, options);
10550
+ return new _Browser(cdp, provider, session, connectOptions);
9196
10551
  }
9197
10552
  /**
9198
10553
  * Get or create a page by name.
@@ -9373,6 +10728,316 @@ function connect(options) {
9373
10728
  return Browser.connect(options);
9374
10729
  }
9375
10730
 
10731
+ // src/browser/index.ts
10732
+ init_combobox();
10733
+
10734
+ // src/browser/fingerprint.ts
10735
+ function createFingerprint(node, context) {
10736
+ const role = node.role?.toLowerCase() ?? "";
10737
+ const name = node.name ?? "";
10738
+ let valueShape = "";
10739
+ if (node.value !== void 0) {
10740
+ valueShape = typeof node.value === "string" ? "text" : typeof node.value === "number" ? "number" : typeof node.value === "boolean" ? "boolean" : "other";
10741
+ }
10742
+ const stableAttrs = {};
10743
+ if (node.properties) {
10744
+ for (const key of ["id", "name", "type", "aria-label"]) {
10745
+ const val = node.properties[key];
10746
+ if (val !== void 0 && val !== null) {
10747
+ stableAttrs[key] = String(val);
10748
+ }
10749
+ }
10750
+ }
10751
+ return {
10752
+ role,
10753
+ name,
10754
+ valueShape,
10755
+ label: name,
10756
+ // label is typically the accessible name
10757
+ stableAttrs,
10758
+ nearestHeading: context.nearestHeading,
10759
+ siblingIndex: context.siblingIndex,
10760
+ sectionPath: [...context.headingTrail]
10761
+ };
10762
+ }
10763
+ function fingerprintKey(fp) {
10764
+ const parts = [fp.role, fp.name, fp.sectionPath.join(">")];
10765
+ if (fp.stableAttrs["id"]) parts.push(`id=${fp.stableAttrs["id"]}`);
10766
+ if (fp.stableAttrs["name"]) parts.push(`name=${fp.stableAttrs["name"]}`);
10767
+ return parts.join("|");
10768
+ }
10769
+ function fingerprintSimilarity(a, b) {
10770
+ let score = 0;
10771
+ let weight = 0;
10772
+ weight += 3;
10773
+ if (a.role === b.role) score += 3;
10774
+ else return 0;
10775
+ weight += 5;
10776
+ if (a.name && b.name && a.name === b.name) score += 5;
10777
+ else if (a.name && b.name && a.name.toLowerCase() === b.name.toLowerCase()) score += 4;
10778
+ weight += 3;
10779
+ const pathA = a.sectionPath.join(">");
10780
+ const pathB = b.sectionPath.join(">");
10781
+ if (pathA === pathB) score += 3;
10782
+ else if (pathA && pathB && (pathA.includes(pathB) || pathB.includes(pathA))) score += 1;
10783
+ const attrKeys = /* @__PURE__ */ new Set([...Object.keys(a.stableAttrs), ...Object.keys(b.stableAttrs)]);
10784
+ for (const key of attrKeys) {
10785
+ weight += 2;
10786
+ if (a.stableAttrs[key] && b.stableAttrs[key] && a.stableAttrs[key] === b.stableAttrs[key]) {
10787
+ score += 2;
10788
+ }
10789
+ }
10790
+ weight += 1;
10791
+ if (a.siblingIndex === b.siblingIndex) score += 1;
10792
+ return score / weight;
10793
+ }
10794
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
10795
+ "button",
10796
+ "link",
10797
+ "textbox",
10798
+ "checkbox",
10799
+ "radio",
10800
+ "combobox",
10801
+ "listbox",
10802
+ "menuitem",
10803
+ "tab",
10804
+ "switch",
10805
+ "searchbox",
10806
+ "spinbutton",
10807
+ "slider"
10808
+ ]);
10809
+ function buildFingerprintMap(nodes) {
10810
+ const map = /* @__PURE__ */ new Map();
10811
+ function walk(nodeList, headingTrail, nearestHeading) {
10812
+ const roleCounts = /* @__PURE__ */ new Map();
10813
+ for (const node of nodeList) {
10814
+ const role = node.role?.toLowerCase() ?? "";
10815
+ let currentHeadingTrail = headingTrail;
10816
+ let currentNearestHeading = nearestHeading;
10817
+ if (role === "heading" && node.name) {
10818
+ currentHeadingTrail = [...headingTrail, node.name];
10819
+ currentNearestHeading = node.name;
10820
+ }
10821
+ if (INTERACTIVE_ROLES.has(role) && node.ref) {
10822
+ const siblingCount = roleCounts.get(role) ?? 0;
10823
+ roleCounts.set(role, siblingCount + 1);
10824
+ const fp = createFingerprint(node, {
10825
+ headingTrail: currentHeadingTrail,
10826
+ siblingIndex: siblingCount,
10827
+ nearestHeading: currentNearestHeading
10828
+ });
10829
+ map.set(node.ref, fp);
10830
+ }
10831
+ if (node.children) {
10832
+ walk(node.children, currentHeadingTrail, currentNearestHeading);
10833
+ }
10834
+ }
10835
+ }
10836
+ walk(nodes, [], "");
10837
+ return map;
10838
+ }
10839
+ function recoverStaleRef(staleFingerprint, currentFingerprints, threshold = 0.7) {
10840
+ let bestRef = null;
10841
+ let bestScore = 0;
10842
+ let secondBestScore = 0;
10843
+ for (const [ref, fp] of currentFingerprints) {
10844
+ const similarity = fingerprintSimilarity(staleFingerprint, fp);
10845
+ if (similarity > bestScore) {
10846
+ secondBestScore = bestScore;
10847
+ bestScore = similarity;
10848
+ bestRef = ref;
10849
+ } else if (similarity > secondBestScore) {
10850
+ secondBestScore = similarity;
10851
+ }
10852
+ }
10853
+ if (!bestRef || bestScore < threshold) return null;
10854
+ if (secondBestScore > 0 && bestScore - secondBestScore < 0.15) return null;
10855
+ return { ref: bestRef, confidence: bestScore };
10856
+ }
10857
+
10858
+ // src/browser/overlay-detect.ts
10859
+ async function detectOverlay(page) {
10860
+ const result = await page.evaluate(`(() => {
10861
+ // Check for role="dialog" or role="alertdialog"
10862
+ const dialogs = document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]');
10863
+ for (const d of dialogs) {
10864
+ if (d.offsetParent !== null || getComputedStyle(d).display !== 'none') {
10865
+ return {
10866
+ hasOverlay: true,
10867
+ overlaySelector: d.id ? '#' + d.id : (d.getAttribute('role') ? '[role="' + d.getAttribute('role') + '"]' : 'dialog'),
10868
+ overlayText: (d.textContent || '').trim().slice(0, 200),
10869
+ };
10870
+ }
10871
+ }
10872
+
10873
+ // Check for fixed/absolute positioned elements with high z-index that look like modals
10874
+ const allElements = document.querySelectorAll('*');
10875
+ for (const el of allElements) {
10876
+ const style = getComputedStyle(el);
10877
+ if (
10878
+ (style.position === 'fixed' || style.position === 'absolute') &&
10879
+ parseInt(style.zIndex || '0', 10) > 999 &&
10880
+ el.offsetWidth > 100 &&
10881
+ el.offsetHeight > 100 &&
10882
+ style.display !== 'none' &&
10883
+ style.visibility !== 'hidden'
10884
+ ) {
10885
+ const text = (el.textContent || '').trim();
10886
+ if (text.length > 10) {
10887
+ return {
10888
+ hasOverlay: true,
10889
+ overlaySelector: el.id ? '#' + el.id : null,
10890
+ overlayText: text.slice(0, 200),
10891
+ };
10892
+ }
10893
+ }
10894
+ }
10895
+
10896
+ return { hasOverlay: false };
10897
+ })()`);
10898
+ return result ?? { hasOverlay: false };
10899
+ }
10900
+
10901
+ // src/browser/safe-submit.ts
10902
+ async function submitAndVerify(page, options) {
10903
+ const {
10904
+ selector,
10905
+ method = "enter+click",
10906
+ expectAny,
10907
+ expectAll,
10908
+ failIf,
10909
+ dangerous = false,
10910
+ timeout = 3e4,
10911
+ waitForNavigation: waitForNavigation2 = "auto"
10912
+ } = options;
10913
+ const startTime = Date.now();
10914
+ const allConditions = [...expectAny ?? [], ...expectAll ?? [], ...failIf ?? []];
10915
+ const needsNetwork = allConditions.some((c) => c.kind === "networkResponse");
10916
+ const needsSignature = allConditions.some((c) => c.kind === "stateSignatureChanges");
10917
+ let networkTracker;
10918
+ let beforeSignature;
10919
+ if (needsNetwork) {
10920
+ networkTracker = new NetworkResponseTracker();
10921
+ networkTracker.start(page.cdpClient);
10922
+ }
10923
+ if (needsSignature) {
10924
+ beforeSignature = await captureStateSignature(page);
10925
+ }
10926
+ try {
10927
+ await page.submit(selector, {
10928
+ timeout,
10929
+ method,
10930
+ waitForNavigation: waitForNavigation2
10931
+ });
10932
+ if (networkTracker) networkTracker.stop(page.cdpClient);
10933
+ if (allConditions.length === 0) {
10934
+ return {
10935
+ submitted: true,
10936
+ outcomeStatus: "success",
10937
+ matchedConditions: [],
10938
+ retrySafe: !dangerous,
10939
+ durationMs: Date.now() - startTime
10940
+ };
10941
+ }
10942
+ const outcome = await evaluateOutcome(page, {
10943
+ expectAny,
10944
+ expectAll,
10945
+ failIf,
10946
+ dangerous,
10947
+ networkTracker,
10948
+ beforeSignature
10949
+ });
10950
+ return {
10951
+ submitted: true,
10952
+ outcomeStatus: outcome.outcomeStatus,
10953
+ matchedConditions: outcome.matchedConditions,
10954
+ retrySafe: outcome.retrySafe,
10955
+ durationMs: Date.now() - startTime
10956
+ };
10957
+ } catch (error) {
10958
+ if (networkTracker) networkTracker.stop(page.cdpClient);
10959
+ return {
10960
+ submitted: false,
10961
+ outcomeStatus: "failed",
10962
+ matchedConditions: [],
10963
+ retrySafe: !dangerous,
10964
+ durationMs: Date.now() - startTime,
10965
+ error: error instanceof Error ? error.message : String(error)
10966
+ };
10967
+ }
10968
+ }
10969
+
10970
+ // src/runtime/clock.ts
10971
+ function now() {
10972
+ return Date.now();
10973
+ }
10974
+
10975
+ // src/browser/target-pin.ts
10976
+ function createTargetFingerprint(targetId, url, title) {
10977
+ return {
10978
+ url,
10979
+ title,
10980
+ originalTargetId: targetId,
10981
+ pinnedAt: now()
10982
+ };
10983
+ }
10984
+ function scoreCandidate(candidate, pin) {
10985
+ if (candidate.targetId === pin.originalTargetId) return 1;
10986
+ let score = 0;
10987
+ if (candidate.url && pin.url) {
10988
+ if (candidate.url === pin.url) {
10989
+ score += 0.6;
10990
+ } else {
10991
+ try {
10992
+ const candidateOrigin = new URL(candidate.url).origin;
10993
+ const pinOrigin = new URL(pin.url).origin;
10994
+ if (candidateOrigin === pinOrigin) score += 0.3;
10995
+ } catch {
10996
+ }
10997
+ }
10998
+ }
10999
+ if (candidate.title && pin.title) {
11000
+ if (candidate.title === pin.title) {
11001
+ score += 0.3;
11002
+ } else if (candidate.title.includes(pin.title) || pin.title.includes(candidate.title)) {
11003
+ score += 0.15;
11004
+ }
11005
+ }
11006
+ if (candidate.type !== "page") score *= 0.5;
11007
+ return Math.min(score, 0.95);
11008
+ }
11009
+ function recoverPinnedTarget(pin, targets, threshold = 0.4) {
11010
+ if (targets.length === 0) return null;
11011
+ let bestTarget = null;
11012
+ let bestScore = 0;
11013
+ for (const target of targets) {
11014
+ const score = scoreCandidate(target, pin);
11015
+ if (score > bestScore) {
11016
+ bestScore = score;
11017
+ bestTarget = target;
11018
+ }
11019
+ }
11020
+ if (!bestTarget || bestScore < threshold) return null;
11021
+ let method;
11022
+ if (bestTarget.targetId === pin.originalTargetId) {
11023
+ method = "exact";
11024
+ } else if (bestTarget.url === pin.url) {
11025
+ method = "url_match";
11026
+ } else if (bestTarget.title === pin.title) {
11027
+ method = "title_match";
11028
+ } else {
11029
+ method = "best_guess";
11030
+ }
11031
+ return {
11032
+ targetId: bestTarget.targetId,
11033
+ method,
11034
+ confidence: bestScore
11035
+ };
11036
+ }
11037
+
11038
+ // src/browser/index.ts
11039
+ init_upload();
11040
+
9376
11041
  // src/emulation/devices.ts
9377
11042
  var devices = {
9378
11043
  "iPhone 14": {
@@ -9591,6 +11256,143 @@ function disableTracing() {
9591
11256
  globalTracer.disable();
9592
11257
  }
9593
11258
  }
11259
+
11260
+ // src/trace/workflow-summary.ts
11261
+ function describeStep(result) {
11262
+ const action = result.action;
11263
+ const selector = result.selectorUsed ?? (Array.isArray(result.selector) ? result.selector[0] : result.selector);
11264
+ switch (action) {
11265
+ case "goto":
11266
+ return "Navigate to page";
11267
+ case "click":
11268
+ return `Click ${selector ? `"${selector}"` : "element"}`;
11269
+ case "fill":
11270
+ return `Fill ${selector ? `"${selector}"` : "field"}`;
11271
+ case "type":
11272
+ return `Type into ${selector ? `"${selector}"` : "field"}`;
11273
+ case "select":
11274
+ return `Select option in ${selector ? `"${selector}"` : "dropdown"}`;
11275
+ case "submit":
11276
+ return `Submit ${selector ? `"${selector}"` : "form"}`;
11277
+ case "check":
11278
+ return `Check ${selector ? `"${selector}"` : "checkbox"}`;
11279
+ case "uncheck":
11280
+ return `Uncheck ${selector ? `"${selector}"` : "checkbox"}`;
11281
+ case "press":
11282
+ return "Press key";
11283
+ case "shortcut":
11284
+ return "Keyboard shortcut";
11285
+ case "hover":
11286
+ return `Hover over ${selector ? `"${selector}"` : "element"}`;
11287
+ case "scroll":
11288
+ return `Scroll ${selector ? `"${selector}"` : "page"}`;
11289
+ case "wait":
11290
+ return `Wait for ${selector ? `"${selector}"` : "condition"}`;
11291
+ case "snapshot":
11292
+ return "Capture accessibility snapshot";
11293
+ case "screenshot":
11294
+ return "Take screenshot";
11295
+ case "forms":
11296
+ return "Enumerate form fields";
11297
+ case "evaluate":
11298
+ return "Execute JavaScript";
11299
+ case "text":
11300
+ return "Extract text content";
11301
+ case "review":
11302
+ return "Extract page review";
11303
+ case "delta":
11304
+ return "Capture page delta";
11305
+ default:
11306
+ return `${action}${selector ? ` "${selector}"` : ""}`;
11307
+ }
11308
+ }
11309
+ function summarizeConditions(conditions) {
11310
+ return conditions.filter((c) => c.detail).map((c) => {
11311
+ const status = c.matched ? "\u2713" : "\u2717";
11312
+ return `${status} ${c.detail}`;
11313
+ });
11314
+ }
11315
+ function buildWorkflowSummary(result) {
11316
+ const steps = result.steps.map((s) => {
11317
+ const step = {
11318
+ step: s.index + 1,
11319
+ description: describeStep(s),
11320
+ success: s.success,
11321
+ durationMs: s.durationMs
11322
+ };
11323
+ if (s.outcomeStatus) {
11324
+ step.outcomeStatus = s.outcomeStatus;
11325
+ step.retrySafe = s.retrySafe;
11326
+ }
11327
+ if (s.matchedConditions && s.matchedConditions.length > 0) {
11328
+ step.outcomeEvidence = summarizeConditions(s.matchedConditions);
11329
+ }
11330
+ if (s.error) step.error = s.error;
11331
+ if (s.suggestion) step.suggestion = s.suggestion;
11332
+ return step;
11333
+ });
11334
+ const succeededSteps = steps.filter((s) => s.success).length;
11335
+ const failedSteps = steps.filter((s) => !s.success).length;
11336
+ const hasUnsafeStep = steps.some(
11337
+ (s) => s.retrySafe === false || s.outcomeStatus === "unsafe_to_retry"
11338
+ );
11339
+ const workflowRetrySafe = !hasUnsafeStep;
11340
+ let verdict;
11341
+ if (result.success) {
11342
+ verdict = `Workflow completed successfully (${succeededSteps}/${steps.length} steps)`;
11343
+ } else if (result.stoppedAtIndex !== void 0) {
11344
+ const failedStep = steps[result.stoppedAtIndex];
11345
+ verdict = `Workflow stopped at step ${result.stoppedAtIndex + 1}: ${failedStep?.description ?? "unknown"}`;
11346
+ if (failedStep?.outcomeStatus) {
11347
+ verdict += ` (outcome: ${failedStep.outcomeStatus})`;
11348
+ }
11349
+ } else {
11350
+ verdict = `Workflow completed with ${failedSteps} failure(s)`;
11351
+ }
11352
+ return {
11353
+ success: result.success,
11354
+ totalSteps: steps.length,
11355
+ succeededSteps,
11356
+ failedSteps,
11357
+ totalDurationMs: result.totalDurationMs,
11358
+ steps,
11359
+ verdict,
11360
+ workflowRetrySafe
11361
+ };
11362
+ }
11363
+ function formatWorkflowSummary(summary) {
11364
+ const lines = [];
11365
+ lines.push(`## Workflow ${summary.success ? "Succeeded" : "Failed"}`);
11366
+ lines.push(`${summary.verdict}`);
11367
+ lines.push(
11368
+ `Duration: ${summary.totalDurationMs}ms | Steps: ${summary.succeededSteps}/${summary.totalSteps} passed`
11369
+ );
11370
+ if (!summary.workflowRetrySafe) {
11371
+ lines.push("\u26A0 Contains unsafe-to-retry steps");
11372
+ }
11373
+ lines.push("");
11374
+ for (const step of summary.steps) {
11375
+ const icon = step.success ? "\u2713" : "\u2717";
11376
+ lines.push(`${icon} Step ${step.step}: ${step.description} (${step.durationMs}ms)`);
11377
+ if (step.outcomeStatus) {
11378
+ lines.push(
11379
+ ` Outcome: ${step.outcomeStatus}${step.retrySafe === false ? " (unsafe to retry)" : ""}`
11380
+ );
11381
+ }
11382
+ if (step.outcomeEvidence) {
11383
+ for (const evidence of step.outcomeEvidence) {
11384
+ lines.push(` ${evidence}`);
11385
+ }
11386
+ }
11387
+ if (step.error) {
11388
+ lines.push(` Error: ${step.error}`);
11389
+ }
11390
+ if (step.suggestion) {
11391
+ lines.push(` \u2192 ${step.suggestion}`);
11392
+ }
11393
+ }
11394
+ return lines.join("\n");
11395
+ }
9594
11396
  // Annotate the CommonJS export names for ESM import in node:
9595
11397
  0 && (module.exports = {
9596
11398
  AudioInput,
@@ -9598,25 +11400,48 @@ function disableTracing() {
9598
11400
  BatchExecutor,
9599
11401
  Browser,
9600
11402
  BrowserBaseProvider,
11403
+ BrowserEndpointResolutionError,
9601
11404
  BrowserlessProvider,
9602
11405
  CDPError,
9603
11406
  ElementNotFoundError,
9604
11407
  GenericProvider,
9605
11408
  NavigationError,
11409
+ NetworkResponseTracker,
9606
11410
  Page,
9607
11411
  RequestInterceptor,
9608
11412
  TimeoutError,
9609
11413
  Tracer,
9610
11414
  addBatchToPage,
9611
11415
  bufferToBase64,
11416
+ buildFingerprintMap,
11417
+ buildLocalBrowserScanTargets,
11418
+ buildWorkflowSummary,
9612
11419
  calculateRMS,
11420
+ captureStateSignature,
11421
+ chooseOption,
11422
+ computeDelta,
11423
+ conditionAll,
11424
+ conditionAny,
11425
+ conditionNot,
11426
+ conditionRace,
9613
11427
  connect,
9614
11428
  createCDPClient,
11429
+ createFingerprint,
9615
11430
  createProvider,
11431
+ createTargetFingerprint,
11432
+ detectOverlay,
9616
11433
  devices,
9617
11434
  disableTracing,
11435
+ discoverLocalBrowsers,
9618
11436
  discoverTargets,
9619
11437
  enableTracing,
11438
+ evaluateCondition,
11439
+ evaluateOutcome,
11440
+ extractPageState,
11441
+ extractReview,
11442
+ fingerprintKey,
11443
+ fingerprintSimilarity,
11444
+ formatWorkflowSummary,
9620
11445
  generateSilence,
9621
11446
  generateTone,
9622
11447
  getAudioChromeFlags,
@@ -9624,9 +11449,16 @@ function disableTracing() {
9624
11449
  getTracer,
9625
11450
  grantAudioPermissions,
9626
11451
  isTranscriptionAvailable,
11452
+ parseDevToolsActivePortFile,
9627
11453
  parseWavHeader,
9628
11454
  pcmToWav,
11455
+ recoverPinnedTarget,
11456
+ recoverStaleRef,
11457
+ resolveBrowserEndpoint,
11458
+ resolveChromeUserDataDirs,
11459
+ submitAndVerify,
9629
11460
  transcribe,
11461
+ uploadFiles,
9630
11462
  validateSteps,
9631
11463
  waitForAnyElement,
9632
11464
  waitForElement,