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
@@ -3,6 +3,275 @@ import {
3
3
  stringifyUnknown
4
4
  } from "./chunk-DTVRFXKI.mjs";
5
5
 
6
+ // src/utils/strings.ts
7
+ function readString(value) {
8
+ return typeof value === "string" ? value : void 0;
9
+ }
10
+ function readStringOr(value, fallback = "") {
11
+ return readString(value) ?? fallback;
12
+ }
13
+ function formatConsoleArg(entry) {
14
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
15
+ }
16
+ function globToRegex(pattern) {
17
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
18
+ const withWildcards = escaped.replace(/\*/g, ".*");
19
+ return new RegExp(`^${withWildcards}$`);
20
+ }
21
+
22
+ // src/actions/conditions.ts
23
+ var NetworkResponseTracker = class {
24
+ responses = [];
25
+ listening = false;
26
+ handler = null;
27
+ start(cdp) {
28
+ if (this.listening) return;
29
+ this.listening = true;
30
+ this.handler = (params) => {
31
+ const response = params["response"];
32
+ if (response) {
33
+ this.responses.push({ url: response.url, status: response.status });
34
+ }
35
+ };
36
+ cdp.on("Network.responseReceived", this.handler);
37
+ }
38
+ stop(cdp) {
39
+ if (this.handler) {
40
+ cdp.off("Network.responseReceived", this.handler);
41
+ this.handler = null;
42
+ }
43
+ this.listening = false;
44
+ }
45
+ getResponses() {
46
+ return this.responses;
47
+ }
48
+ reset() {
49
+ this.responses = [];
50
+ }
51
+ };
52
+ async function captureStateSignature(page) {
53
+ try {
54
+ const url = await page.url();
55
+ const text = await page.text();
56
+ const truncated = text.slice(0, 2e3);
57
+ return `${url}|${simpleHash(truncated)}`;
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
62
+ function simpleHash(str) {
63
+ let hash = 0;
64
+ for (let i = 0; i < str.length; i++) {
65
+ const char = str.charCodeAt(i);
66
+ hash = (hash << 5) - hash + char | 0;
67
+ }
68
+ return hash.toString(36);
69
+ }
70
+ async function evaluateCondition(condition, page, context = {}) {
71
+ switch (condition.kind) {
72
+ case "urlMatches": {
73
+ try {
74
+ const currentUrl = await page.url();
75
+ const regex = globToRegex(condition.pattern);
76
+ const matched = regex.test(currentUrl);
77
+ return {
78
+ condition,
79
+ matched,
80
+ detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
81
+ };
82
+ } catch {
83
+ return { condition, matched: false, detail: "Failed to get current URL" };
84
+ }
85
+ }
86
+ case "elementVisible": {
87
+ try {
88
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
89
+ for (const sel of selectors) {
90
+ const visible = await page.waitFor(sel, {
91
+ timeout: 2e3,
92
+ optional: true,
93
+ state: "visible"
94
+ });
95
+ if (visible) {
96
+ return { condition, matched: true, detail: `Element "${sel}" is visible` };
97
+ }
98
+ }
99
+ return { condition, matched: false, detail: "No matching visible element found" };
100
+ } catch {
101
+ return { condition, matched: false, detail: "Visibility check failed" };
102
+ }
103
+ }
104
+ case "elementHidden": {
105
+ try {
106
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
107
+ for (const sel of selectors) {
108
+ const visible = await page.waitFor(sel, {
109
+ timeout: 500,
110
+ optional: true,
111
+ state: "visible"
112
+ });
113
+ if (visible) {
114
+ return { condition, matched: false, detail: `Element "${sel}" is still visible` };
115
+ }
116
+ }
117
+ return { condition, matched: true, detail: "Element is hidden or not found" };
118
+ } catch {
119
+ return { condition, matched: true, detail: "Element is hidden (check threw)" };
120
+ }
121
+ }
122
+ case "textAppears": {
123
+ try {
124
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
125
+ const text = await page.text(selector);
126
+ const matched = text.includes(condition.text);
127
+ return {
128
+ condition,
129
+ matched,
130
+ detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
131
+ };
132
+ } catch {
133
+ return { condition, matched: false, detail: "Failed to get page text" };
134
+ }
135
+ }
136
+ case "textChanges": {
137
+ try {
138
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
139
+ const text = await page.text(selector);
140
+ if (condition.to !== void 0) {
141
+ const matched = text.includes(condition.to);
142
+ return {
143
+ condition,
144
+ matched,
145
+ detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
146
+ };
147
+ }
148
+ return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
149
+ } catch {
150
+ return { condition, matched: false, detail: "Failed to get text for change detection" };
151
+ }
152
+ }
153
+ case "networkResponse": {
154
+ const tracker = context.networkTracker;
155
+ if (!tracker) {
156
+ return { condition, matched: false, detail: "No network tracker active" };
157
+ }
158
+ const regex = globToRegex(condition.urlPattern);
159
+ const responses = tracker.getResponses();
160
+ for (const resp of responses) {
161
+ if (regex.test(resp.url)) {
162
+ if (condition.status !== void 0 && resp.status !== condition.status) {
163
+ continue;
164
+ }
165
+ return {
166
+ condition,
167
+ matched: true,
168
+ detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
169
+ };
170
+ }
171
+ }
172
+ return {
173
+ condition,
174
+ matched: false,
175
+ detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
176
+ };
177
+ }
178
+ case "stateSignatureChanges": {
179
+ if (!context.beforeSignature) {
180
+ return { condition, matched: false, detail: "No before-signature captured" };
181
+ }
182
+ const afterSignature = await captureStateSignature(page);
183
+ const matched = afterSignature !== context.beforeSignature;
184
+ return {
185
+ condition,
186
+ matched,
187
+ detail: matched ? "Page state changed" : "Page state unchanged"
188
+ };
189
+ }
190
+ default: {
191
+ const _exhaustive = condition;
192
+ return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
193
+ }
194
+ }
195
+ }
196
+ async function evaluateOutcome(page, options) {
197
+ const {
198
+ expectAny,
199
+ expectAll,
200
+ failIf,
201
+ dangerous = false,
202
+ networkTracker,
203
+ beforeSignature
204
+ } = options;
205
+ const allMatched = [];
206
+ const context = { networkTracker, beforeSignature };
207
+ if (failIf && failIf.length > 0) {
208
+ for (const condition of failIf) {
209
+ const result = await evaluateCondition(condition, page, context);
210
+ allMatched.push(result);
211
+ if (result.matched) {
212
+ return {
213
+ outcomeStatus: "failed",
214
+ matchedConditions: allMatched,
215
+ retrySafe: !dangerous
216
+ };
217
+ }
218
+ }
219
+ }
220
+ if (expectAll && expectAll.length > 0) {
221
+ let allPassed = true;
222
+ for (const condition of expectAll) {
223
+ const result = await evaluateCondition(condition, page, context);
224
+ allMatched.push(result);
225
+ if (!result.matched) {
226
+ allPassed = false;
227
+ }
228
+ }
229
+ if (!allPassed) {
230
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
231
+ return {
232
+ outcomeStatus: status,
233
+ matchedConditions: allMatched,
234
+ retrySafe: !dangerous
235
+ };
236
+ }
237
+ if (!expectAny || expectAny.length === 0) {
238
+ return {
239
+ outcomeStatus: "success",
240
+ matchedConditions: allMatched,
241
+ retrySafe: true
242
+ };
243
+ }
244
+ }
245
+ if (expectAny && expectAny.length > 0) {
246
+ let anyPassed = false;
247
+ for (const condition of expectAny) {
248
+ const result = await evaluateCondition(condition, page, context);
249
+ allMatched.push(result);
250
+ if (result.matched) {
251
+ anyPassed = true;
252
+ }
253
+ }
254
+ if (anyPassed) {
255
+ return {
256
+ outcomeStatus: "success",
257
+ matchedConditions: allMatched,
258
+ retrySafe: true
259
+ };
260
+ }
261
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
262
+ return {
263
+ outcomeStatus: status,
264
+ matchedConditions: allMatched,
265
+ retrySafe: !dangerous
266
+ };
267
+ }
268
+ return {
269
+ outcomeStatus: "success",
270
+ matchedConditions: allMatched,
271
+ retrySafe: true
272
+ };
273
+ }
274
+
6
275
  // src/actions/executor.ts
7
276
  import * as fs from "fs";
8
277
  import { join } from "path";
@@ -866,6 +1135,7 @@ function buildTraceSummary(events, view) {
866
1135
  case "session":
867
1136
  return summarizeSession(events);
868
1137
  }
1138
+ throw new Error(`Unsupported trace view: ${view}`);
869
1139
  }
870
1140
  function buildTraceSummaries(events) {
871
1141
  return {
@@ -879,7 +1149,9 @@ function buildTraceSummaries(events) {
879
1149
  };
880
1150
  }
881
1151
  function summarizeWs(events) {
882
- const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
1152
+ const relevant = events.filter(
1153
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
1154
+ );
883
1155
  const connections = /* @__PURE__ */ new Map();
884
1156
  for (const event of relevant) {
885
1157
  const id = event.connectionId ?? event.requestId ?? event.traceId;
@@ -909,7 +1181,7 @@ function summarizeWs(events) {
909
1181
  }
910
1182
  const values = [...connections.values()];
911
1183
  const reconnects = values.reduce((count, connection) => {
912
- return connection.closedAt && !connection.createdAt ? count : count;
1184
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
913
1185
  }, 0);
914
1186
  return {
915
1187
  view: "ws",
@@ -1162,6 +1434,31 @@ function frameToStep(frame) {
1162
1434
  }
1163
1435
  }
1164
1436
 
1437
+ // src/trace/model.ts
1438
+ function createTraceId(prefix = "evt") {
1439
+ const random = Math.random().toString(36).slice(2, 10);
1440
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1441
+ }
1442
+ function normalizeTraceEvent(event) {
1443
+ return {
1444
+ traceId: event.traceId ?? createTraceId(event.channel),
1445
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1446
+ elapsedMs: event.elapsedMs ?? 0,
1447
+ severity: event.severity ?? inferSeverity(event.event),
1448
+ data: event.data ?? {},
1449
+ ...event
1450
+ };
1451
+ }
1452
+ function inferSeverity(eventName) {
1453
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1454
+ return "error";
1455
+ }
1456
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1457
+ return "warn";
1458
+ }
1459
+ return "info";
1460
+ }
1461
+
1165
1462
  // src/trace/script.ts
1166
1463
  var TRACE_BINDING_NAME = "__bpTraceBinding";
1167
1464
  var TRACE_SCRIPT = `
@@ -1441,298 +1738,6 @@ var TRACE_SCRIPT = `
1441
1738
  })();
1442
1739
  `;
1443
1740
 
1444
- // src/trace/model.ts
1445
- function createTraceId(prefix = "evt") {
1446
- const random = Math.random().toString(36).slice(2, 10);
1447
- return `${prefix}-${Date.now().toString(36)}-${random}`;
1448
- }
1449
- function normalizeTraceEvent(event) {
1450
- return {
1451
- traceId: event.traceId ?? createTraceId(event.channel),
1452
- ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1453
- elapsedMs: event.elapsedMs ?? 0,
1454
- severity: event.severity ?? inferSeverity(event.event),
1455
- data: event.data ?? {},
1456
- ...event
1457
- };
1458
- }
1459
- function inferSeverity(eventName) {
1460
- if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1461
- return "error";
1462
- }
1463
- if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1464
- return "warn";
1465
- }
1466
- return "info";
1467
- }
1468
-
1469
- // src/trace/live.ts
1470
- function globToRegex(pattern) {
1471
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1472
- const withWildcards = escaped.replace(/\*/g, ".*");
1473
- return new RegExp(`^${withWildcards}$`);
1474
- }
1475
- var LiveTraceCollector = class {
1476
- cdp;
1477
- options;
1478
- handlers = [];
1479
- wsUrls = /* @__PURE__ */ new Map();
1480
- httpUrls = /* @__PURE__ */ new Map();
1481
- events = [];
1482
- startTime = Date.now();
1483
- matchRegex;
1484
- constructor(cdp, options = {}) {
1485
- this.cdp = cdp;
1486
- this.options = options;
1487
- this.matchRegex = options.match ? globToRegex(options.match) : null;
1488
- }
1489
- async start() {
1490
- await this.cdp.send("Runtime.enable");
1491
- await this.cdp.send("Page.enable");
1492
- await this.cdp.send("Network.enable");
1493
- await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
1494
- await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
1495
- await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
1496
- if ((this.options.mode ?? "all") !== "http") {
1497
- this.subscribe("Network.webSocketCreated", (params) => {
1498
- const requestId = String(params["requestId"] ?? "");
1499
- const url = String(params["url"] ?? "");
1500
- if (!this.matchesUrl(url)) {
1501
- return;
1502
- }
1503
- this.wsUrls.set(requestId, url);
1504
- void this.emit({
1505
- channel: "ws",
1506
- event: "ws.connection.created",
1507
- summary: `WebSocket opened ${url}`,
1508
- connectionId: requestId,
1509
- requestId,
1510
- url,
1511
- data: { url }
1512
- });
1513
- });
1514
- this.subscribe("Network.webSocketFrameSent", (params) => {
1515
- const requestId = String(params["requestId"] ?? "");
1516
- const response = params["response"];
1517
- const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
1518
- const url = this.wsUrls.get(requestId);
1519
- if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
1520
- return;
1521
- }
1522
- void this.emit({
1523
- channel: "ws",
1524
- event: "ws.frame.sent",
1525
- summary: `WebSocket frame sent ${requestId}`,
1526
- connectionId: requestId,
1527
- requestId,
1528
- url,
1529
- data: {
1530
- opcode: response?.opcode ?? 1,
1531
- payload,
1532
- length: response?.payloadData?.length ?? 0
1533
- }
1534
- });
1535
- });
1536
- this.subscribe("Network.webSocketFrameReceived", (params) => {
1537
- const requestId = String(params["requestId"] ?? "");
1538
- const response = params["response"];
1539
- const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
1540
- const url = this.wsUrls.get(requestId);
1541
- if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
1542
- return;
1543
- }
1544
- void this.emit({
1545
- channel: "ws",
1546
- event: "ws.frame.received",
1547
- summary: `WebSocket frame received ${requestId}`,
1548
- connectionId: requestId,
1549
- requestId,
1550
- url,
1551
- data: {
1552
- opcode: response?.opcode ?? 1,
1553
- payload,
1554
- length: response?.payloadData?.length ?? 0
1555
- }
1556
- });
1557
- });
1558
- this.subscribe("Network.webSocketClosed", (params) => {
1559
- const requestId = String(params["requestId"] ?? "");
1560
- const url = this.wsUrls.get(requestId);
1561
- this.wsUrls.delete(requestId);
1562
- void this.emit({
1563
- channel: "ws",
1564
- event: "ws.connection.closed",
1565
- summary: `WebSocket closed ${requestId}`,
1566
- severity: "warn",
1567
- connectionId: requestId,
1568
- requestId,
1569
- url,
1570
- data: { url }
1571
- });
1572
- });
1573
- }
1574
- if ((this.options.mode ?? "all") !== "ws") {
1575
- this.subscribe("Network.requestWillBeSent", (params) => {
1576
- const request = params["request"];
1577
- const requestId = String(params["requestId"] ?? "");
1578
- const url = String(request?.url ?? "");
1579
- if (!this.matchesUrl(url)) {
1580
- return;
1581
- }
1582
- this.httpUrls.set(requestId, url);
1583
- void this.emit({
1584
- channel: "http",
1585
- event: "http.request.sent",
1586
- summary: `${request?.method ?? "GET"} ${url}`,
1587
- requestId,
1588
- url,
1589
- data: {
1590
- method: request?.method ?? "GET",
1591
- headers: request?.headers ?? {},
1592
- body: request?.postData ?? null
1593
- }
1594
- });
1595
- });
1596
- this.subscribe("Network.responseReceived", (params) => {
1597
- const requestId = String(params["requestId"] ?? "");
1598
- if (!this.httpUrls.has(requestId)) {
1599
- return;
1600
- }
1601
- const response = params["response"];
1602
- void this.emit({
1603
- channel: "http",
1604
- event: "http.response.received",
1605
- summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
1606
- requestId,
1607
- url: response?.url ?? this.httpUrls.get(requestId),
1608
- data: {
1609
- status: response?.status ?? 0,
1610
- headers: response?.headers ?? {},
1611
- mimeType: response?.mimeType ?? null
1612
- }
1613
- });
1614
- });
1615
- this.subscribe("Network.loadingFailed", (params) => {
1616
- const requestId = String(params["requestId"] ?? "");
1617
- const url = String(params["blockedReason"] ?? this.httpUrls.get(requestId) ?? "");
1618
- void this.emit({
1619
- channel: "http",
1620
- event: "http.response.failed",
1621
- summary: `HTTP request failed ${requestId}`,
1622
- severity: "error",
1623
- requestId,
1624
- url,
1625
- data: {
1626
- errorText: params["errorText"] ?? null,
1627
- blockedReason: params["blockedReason"] ?? null,
1628
- canceled: params["canceled"] ?? false
1629
- }
1630
- });
1631
- });
1632
- }
1633
- this.subscribe("Runtime.consoleAPICalled", (params) => {
1634
- const type = String(params["type"] ?? "log");
1635
- if (type !== "log" && type !== "warn" && type !== "error") {
1636
- return;
1637
- }
1638
- const args = Array.isArray(params["args"]) ? params["args"] : [];
1639
- const text = args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ");
1640
- void this.emit({
1641
- channel: "console",
1642
- event: `console.${type}`,
1643
- severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
1644
- summary: text || `console.${type}`,
1645
- data: { args }
1646
- });
1647
- });
1648
- this.subscribe("Runtime.exceptionThrown", (params) => {
1649
- const details = params["exceptionDetails"] ?? {};
1650
- const text = String(details["text"] ?? "Runtime exception");
1651
- void this.emit({
1652
- channel: "runtime",
1653
- event: "runtime.exception",
1654
- severity: "error",
1655
- summary: text,
1656
- data: details
1657
- });
1658
- });
1659
- this.subscribe("Runtime.bindingCalled", (params) => {
1660
- if (params["name"] !== TRACE_BINDING_NAME) {
1661
- return;
1662
- }
1663
- const raw = String(params["payload"] ?? "");
1664
- try {
1665
- const payload = JSON.parse(raw);
1666
- const channel = this.channelForTraceEvent(payload.event);
1667
- void this.emit({
1668
- channel,
1669
- event: payload.event,
1670
- severity: payload.severity,
1671
- summary: payload.summary ?? payload.event,
1672
- ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
1673
- data: payload.data ?? {},
1674
- url: typeof payload.data?.["url"] === "string" ? payload.data["url"] : void 0
1675
- });
1676
- } catch {
1677
- }
1678
- });
1679
- }
1680
- async stop() {
1681
- for (const { event, handler } of this.handlers) {
1682
- this.cdp.off(event, handler);
1683
- }
1684
- this.handlers.length = 0;
1685
- return [...this.events];
1686
- }
1687
- getEvents() {
1688
- return [...this.events];
1689
- }
1690
- subscribe(event, handler) {
1691
- this.cdp.on(event, handler);
1692
- this.handlers.push({ event, handler });
1693
- }
1694
- matchesUrl(url) {
1695
- if (!this.matchRegex) {
1696
- return true;
1697
- }
1698
- return this.matchRegex.test(url);
1699
- }
1700
- formatPayload(payloadData, opcode) {
1701
- const data = payloadData ?? "";
1702
- const maxPayload = this.options.maxPayload ?? 256;
1703
- if (opcode === 2) {
1704
- const byteLength = Math.floor(data.length * 3 / 4);
1705
- return `[binary: ${byteLength} bytes]`;
1706
- }
1707
- if (data.length > maxPayload) {
1708
- return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
1709
- }
1710
- return data;
1711
- }
1712
- channelForTraceEvent(eventName) {
1713
- if (eventName.startsWith("ws.")) return "ws";
1714
- if (eventName.startsWith("http.")) return "http";
1715
- if (eventName.startsWith("console.")) return "console";
1716
- if (eventName.startsWith("permission.")) return "permission";
1717
- if (eventName.startsWith("media.")) return "media";
1718
- if (eventName.startsWith("voice.")) return "voice";
1719
- if (eventName.startsWith("dom.")) return "dom";
1720
- if (eventName.startsWith("runtime.")) return "runtime";
1721
- return "session";
1722
- }
1723
- async emit(event) {
1724
- const normalized = normalizeTraceEvent({
1725
- traceId: event.traceId ?? createTraceId(event.channel),
1726
- sessionId: this.options.sessionId,
1727
- targetId: this.options.targetId,
1728
- elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
1729
- ...event
1730
- });
1731
- this.events.push(normalized);
1732
- await this.options.onEvent?.(normalized);
1733
- }
1734
- };
1735
-
1736
1741
  // src/actions/executor.ts
1737
1742
  var DEFAULT_TIMEOUT = 3e4;
1738
1743
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -1855,6 +1860,25 @@ function getSuggestion(reason) {
1855
1860
  }
1856
1861
  }
1857
1862
  }
1863
+ function hasOutcomeConditions(step) {
1864
+ 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;
1865
+ }
1866
+ function needsNetworkTracking(step) {
1867
+ const allConditions = [
1868
+ ...step.expectAny ?? [],
1869
+ ...step.expectAll ?? [],
1870
+ ...step.failIf ?? []
1871
+ ];
1872
+ return allConditions.some((c) => c.kind === "networkResponse");
1873
+ }
1874
+ function needsStateSignature(step) {
1875
+ const allConditions = [
1876
+ ...step.expectAny ?? [],
1877
+ ...step.expectAll ?? [],
1878
+ ...step.failIf ?? []
1879
+ ];
1880
+ return allConditions.some((c) => c.kind === "stateSignatureChanges");
1881
+ }
1858
1882
  var BatchExecutor = class {
1859
1883
  page;
1860
1884
  constructor(page) {
@@ -1900,9 +1924,25 @@ var BatchExecutor = class {
1900
1924
  })
1901
1925
  );
1902
1926
  }
1927
+ const hasOutcome = hasOutcomeConditions(step);
1928
+ let networkTracker;
1929
+ let beforeSignature;
1930
+ if (hasOutcome) {
1931
+ if (needsNetworkTracking(step)) {
1932
+ networkTracker = new NetworkResponseTracker();
1933
+ networkTracker.start(this.page.cdpClient);
1934
+ }
1935
+ if (needsStateSignature(step)) {
1936
+ beforeSignature = await captureStateSignature(this.page);
1937
+ }
1938
+ }
1903
1939
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1904
1940
  if (attempt > 0) {
1905
1941
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
1942
+ if (networkTracker) networkTracker.reset();
1943
+ if (hasOutcome && needsStateSignature(step)) {
1944
+ beforeSignature = await captureStateSignature(this.page);
1945
+ }
1906
1946
  }
1907
1947
  try {
1908
1948
  this.page.resetLastActionPosition();
@@ -1920,6 +1960,28 @@ var BatchExecutor = class {
1920
1960
  coordinates: this.page.getLastActionCoordinates() ?? void 0,
1921
1961
  boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1922
1962
  };
1963
+ if (hasOutcome) {
1964
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
1965
+ const outcome = await evaluateOutcome(this.page, {
1966
+ expectAny: step.expectAny,
1967
+ expectAll: step.expectAll,
1968
+ failIf: step.failIf,
1969
+ dangerous: step.dangerous,
1970
+ networkTracker,
1971
+ beforeSignature
1972
+ });
1973
+ stepResult.outcomeStatus = outcome.outcomeStatus;
1974
+ stepResult.matchedConditions = outcome.matchedConditions;
1975
+ stepResult.retrySafe = outcome.retrySafe;
1976
+ if (outcome.outcomeStatus !== "success") {
1977
+ stepResult.success = false;
1978
+ stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
1979
+ const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
1980
+ if (failedDetails.length > 0) {
1981
+ stepResult.suggestion = failedDetails.join("; ");
1982
+ }
1983
+ }
1984
+ }
1923
1985
  if (recording && !recording.skipActions.has(step.action)) {
1924
1986
  await this.captureRecordingFrame(step, stepResult, recording);
1925
1987
  }
@@ -1929,13 +1991,14 @@ var BatchExecutor = class {
1929
1991
  traceId: createTraceId("action"),
1930
1992
  elapsedMs: Date.now() - startTime,
1931
1993
  channel: "action",
1932
- event: "action.succeeded",
1933
- summary: `${step.action} succeeded`,
1994
+ event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
1995
+ summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
1934
1996
  data: {
1935
1997
  action: step.action,
1936
1998
  selector: step.selector ?? null,
1937
1999
  selectorUsed: result.selectorUsed ?? null,
1938
- durationMs: Date.now() - stepStart
2000
+ durationMs: Date.now() - stepStart,
2001
+ outcomeStatus: stepResult.outcomeStatus ?? null
1939
2002
  },
1940
2003
  actionId: `action-${i + 1}`,
1941
2004
  stepIndex: i,
@@ -1945,6 +2008,18 @@ var BatchExecutor = class {
1945
2008
  })
1946
2009
  );
1947
2010
  }
2011
+ if (hasOutcome && !stepResult.success) {
2012
+ if (step.dangerous) {
2013
+ results.push(stepResult);
2014
+ break;
2015
+ }
2016
+ if (attempt < maxAttempts - 1) {
2017
+ lastError = new Error(stepResult.error ?? "Outcome failed");
2018
+ continue;
2019
+ }
2020
+ results.push(stepResult);
2021
+ break;
2022
+ }
1948
2023
  results.push(stepResult);
1949
2024
  succeeded = true;
1950
2025
  break;
@@ -1952,59 +2027,63 @@ var BatchExecutor = class {
1952
2027
  lastError = error instanceof Error ? error : new Error(String(error));
1953
2028
  }
1954
2029
  }
2030
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
1955
2031
  if (!succeeded) {
1956
- const errorMessage = lastError?.message ?? "Unknown error";
1957
- let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
1958
- const { reason, coveringElement } = classifyFailure(lastError);
1959
- if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
1960
- try {
1961
- const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
1962
- const autoHints = await generateHints(this.page, selectors, step.action, 3);
1963
- if (autoHints.length > 0) {
1964
- hints = autoHints;
2032
+ const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
2033
+ if (!resultAlreadyPushed) {
2034
+ const errorMessage = lastError?.message ?? "Unknown error";
2035
+ let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
2036
+ const { reason, coveringElement } = classifyFailure(lastError);
2037
+ if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
2038
+ try {
2039
+ const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
2040
+ const autoHints = await generateHints(this.page, selectors, step.action, 3);
2041
+ if (autoHints.length > 0) {
2042
+ hints = autoHints;
2043
+ }
2044
+ } catch {
1965
2045
  }
1966
- } catch {
1967
2046
  }
2047
+ const failedResult = {
2048
+ index: i,
2049
+ action: step.action,
2050
+ selector: step.selector,
2051
+ success: false,
2052
+ durationMs: Date.now() - stepStart,
2053
+ error: errorMessage,
2054
+ hints,
2055
+ failureReason: reason,
2056
+ coveringElement,
2057
+ suggestion: getSuggestion(reason),
2058
+ timestamp: Date.now()
2059
+ };
2060
+ if (recording && !recording.skipActions.has(step.action)) {
2061
+ await this.captureRecordingFrame(step, failedResult, recording);
2062
+ }
2063
+ if (recording) {
2064
+ recording.traceEvents.push(
2065
+ normalizeTraceEvent({
2066
+ traceId: createTraceId("action"),
2067
+ elapsedMs: Date.now() - startTime,
2068
+ channel: "action",
2069
+ event: "action.failed",
2070
+ severity: "error",
2071
+ summary: `${step.action} failed: ${errorMessage}`,
2072
+ data: {
2073
+ action: step.action,
2074
+ selector: step.selector ?? null,
2075
+ error: errorMessage,
2076
+ reason
2077
+ },
2078
+ actionId: `action-${i + 1}`,
2079
+ stepIndex: i,
2080
+ selector: step.selector,
2081
+ url: step.url
2082
+ })
2083
+ );
2084
+ }
2085
+ results.push(failedResult);
1968
2086
  }
1969
- const failedResult = {
1970
- index: i,
1971
- action: step.action,
1972
- selector: step.selector,
1973
- success: false,
1974
- durationMs: Date.now() - stepStart,
1975
- error: errorMessage,
1976
- hints,
1977
- failureReason: reason,
1978
- coveringElement,
1979
- suggestion: getSuggestion(reason),
1980
- timestamp: Date.now()
1981
- };
1982
- if (recording && !recording.skipActions.has(step.action)) {
1983
- await this.captureRecordingFrame(step, failedResult, recording);
1984
- }
1985
- if (recording) {
1986
- recording.traceEvents.push(
1987
- normalizeTraceEvent({
1988
- traceId: createTraceId("action"),
1989
- elapsedMs: Date.now() - startTime,
1990
- channel: "action",
1991
- event: "action.failed",
1992
- severity: "error",
1993
- summary: `${step.action} failed: ${errorMessage}`,
1994
- data: {
1995
- action: step.action,
1996
- selector: step.selector ?? null,
1997
- error: errorMessage,
1998
- reason
1999
- },
2000
- actionId: `action-${i + 1}`,
2001
- stepIndex: i,
2002
- selector: step.selector,
2003
- url: step.url
2004
- })
2005
- );
2006
- }
2007
- results.push(failedResult);
2008
2087
  if (onFail === "stop" && !step.optional) {
2009
2088
  stoppedAtIndex = i;
2010
2089
  break;
@@ -2315,6 +2394,14 @@ var BatchExecutor = class {
2315
2394
  case "forms": {
2316
2395
  return { value: await this.page.forms() };
2317
2396
  }
2397
+ case "delta": {
2398
+ const review = await this.page.review();
2399
+ return { value: review };
2400
+ }
2401
+ case "review": {
2402
+ const review = await this.page.review();
2403
+ return { value: review };
2404
+ }
2318
2405
  case "screenshot": {
2319
2406
  const data = await this.page.screenshot({
2320
2407
  format: step.format,
@@ -2465,6 +2552,35 @@ var BatchExecutor = class {
2465
2552
  const media = await this.assertMediaTrackLive(step.kind);
2466
2553
  return { value: media };
2467
2554
  }
2555
+ case "chooseOption": {
2556
+ const { chooseOption } = await import("./combobox-RAKBA2BW.mjs");
2557
+ if (!step.value) throw new Error("chooseOption requires value");
2558
+ const result = await chooseOption(this.page, {
2559
+ trigger: step.trigger ?? step.selector ?? "",
2560
+ listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
2561
+ value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
2562
+ match: step.match,
2563
+ timeout: step.timeout ?? timeout
2564
+ });
2565
+ if (!result.success) {
2566
+ throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
2567
+ }
2568
+ return { value: result };
2569
+ }
2570
+ case "upload": {
2571
+ const { uploadFiles } = await import("./upload-E6MCC2OF.mjs");
2572
+ if (!step.selector) throw new Error("upload requires selector");
2573
+ if (!step.files || step.files.length === 0) throw new Error("upload requires files");
2574
+ const result = await uploadFiles(this.page, {
2575
+ selector: step.selector,
2576
+ files: step.files,
2577
+ timeout: step.timeout ?? timeout
2578
+ });
2579
+ if (!result.accepted) {
2580
+ throw new Error(result.error ?? "Upload was not accepted");
2581
+ }
2582
+ return { value: result };
2583
+ }
2468
2584
  default: {
2469
2585
  const action = step.action;
2470
2586
  const aliases = {
@@ -2542,8 +2658,13 @@ Valid actions: ${valid}`);
2542
2658
  await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2543
2659
  } catch {
2544
2660
  }
2545
- await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
2546
- await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
2661
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
2662
+ source: TRACE_SCRIPT
2663
+ });
2664
+ await this.page.cdpClient.send("Runtime.evaluate", {
2665
+ expression: TRACE_SCRIPT,
2666
+ awaitPromise: false
2667
+ });
2547
2668
  }
2548
2669
  async waitForWsMessage(match, where, timeout) {
2549
2670
  await this.ensureTraceHooks();
@@ -2561,12 +2682,12 @@ Valid actions: ${valid}`);
2561
2682
  clearTimeout(timer);
2562
2683
  };
2563
2684
  const onCreated = (params) => {
2564
- wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
2685
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
2565
2686
  };
2566
2687
  const onFrame = (params) => {
2567
- const requestId = String(params["requestId"] ?? "");
2688
+ const requestId = readStringOr(params["requestId"]);
2568
2689
  const response = params["response"] ?? {};
2569
- const payload = String(response.payloadData ?? "");
2690
+ const payload = response.payloadData ?? "";
2570
2691
  const url = wsUrls.get(requestId) ?? "";
2571
2692
  if (!regex.test(url) && !regex.test(payload)) {
2572
2693
  return;
@@ -2582,13 +2703,13 @@ Valid actions: ${valid}`);
2582
2703
  return;
2583
2704
  }
2584
2705
  try {
2585
- const parsed = JSON.parse(String(params["payload"] ?? ""));
2706
+ const parsed = JSON.parse(readStringOr(params["payload"]));
2586
2707
  if (parsed.event !== "ws.frame.received") {
2587
2708
  return;
2588
2709
  }
2589
2710
  const data = parsed.data ?? {};
2590
- const payload = String(data["payload"] ?? "");
2591
- const url = String(data["url"] ?? "");
2711
+ const payload = readStringOr(data["payload"]);
2712
+ const url = readStringOr(data["url"]);
2592
2713
  if (!regex.test(url) && !regex.test(payload)) {
2593
2714
  return;
2594
2715
  }
@@ -2597,7 +2718,7 @@ Valid actions: ${valid}`);
2597
2718
  }
2598
2719
  cleanup();
2599
2720
  resolve({
2600
- requestId: String(data["connectionId"] ?? ""),
2721
+ requestId: readStringOr(data["connectionId"]),
2601
2722
  url,
2602
2723
  payload
2603
2724
  });
@@ -2641,13 +2762,14 @@ Valid actions: ${valid}`);
2641
2762
  if (!entry || typeof entry !== "object") {
2642
2763
  continue;
2643
2764
  }
2644
- const event = String(entry["event"] ?? "");
2765
+ const record = entry;
2766
+ const event = readStringOr(record["event"]);
2645
2767
  if (event !== "ws.frame.received") {
2646
2768
  continue;
2647
2769
  }
2648
- const data = entry["data"] ?? {};
2649
- const payload = String(data["payload"] ?? "");
2650
- const url = String(data["url"] ?? "");
2770
+ const data = record["data"] ?? {};
2771
+ const payload = readStringOr(data["payload"]);
2772
+ const url = readStringOr(data["url"]);
2651
2773
  if (!regex.test(url) && !regex.test(payload)) {
2652
2774
  continue;
2653
2775
  }
@@ -2655,7 +2777,7 @@ Valid actions: ${valid}`);
2655
2777
  continue;
2656
2778
  }
2657
2779
  return {
2658
- requestId: String(data["connectionId"] ?? ""),
2780
+ requestId: readStringOr(data["connectionId"]),
2659
2781
  url,
2660
2782
  payload
2661
2783
  };
@@ -2676,13 +2798,11 @@ Valid actions: ${valid}`);
2676
2798
  return;
2677
2799
  }
2678
2800
  const args = Array.isArray(params["args"]) ? params["args"] : [];
2679
- errors.push(
2680
- args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
2681
- );
2801
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
2682
2802
  };
2683
2803
  const onException = (params) => {
2684
2804
  const details = params["exceptionDetails"] ?? {};
2685
- errors.push(String(details["text"] ?? "Runtime exception"));
2805
+ errors.push(readString(details["text"]) ?? "Runtime exception");
2686
2806
  };
2687
2807
  const timer = setTimeout(() => {
2688
2808
  cleanup();
@@ -3054,6 +3174,30 @@ var ACTION_RULES = {
3054
3174
  kind: { type: "string", enum: ["audio", "video"] }
3055
3175
  },
3056
3176
  optional: {}
3177
+ },
3178
+ delta: {
3179
+ required: {},
3180
+ optional: {}
3181
+ },
3182
+ review: {
3183
+ required: {},
3184
+ optional: {}
3185
+ },
3186
+ chooseOption: {
3187
+ required: { value: { type: "string|string[]" } },
3188
+ optional: {
3189
+ trigger: { type: "string|string[]" },
3190
+ selector: { type: "string|string[]" },
3191
+ option: { type: "string|string[]" },
3192
+ match: { type: "string", enum: ["exact", "contains", "startsWith"] }
3193
+ }
3194
+ },
3195
+ upload: {
3196
+ required: {
3197
+ selector: { type: "string|string[]" },
3198
+ files: { type: "string|string[]" }
3199
+ },
3200
+ optional: {}
3057
3201
  }
3058
3202
  };
3059
3203
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -3093,7 +3237,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
3093
3237
  "name",
3094
3238
  "state",
3095
3239
  "kind",
3096
- "windowMs"
3240
+ "windowMs",
3241
+ "expectAny",
3242
+ "expectAll",
3243
+ "failIf",
3244
+ "dangerous",
3245
+ "files"
3097
3246
  ]);
3098
3247
  function resolveAction(name) {
3099
3248
  if (VALID_ACTIONS.includes(name)) {
@@ -3313,6 +3462,64 @@ function validateSteps(steps) {
3313
3462
  });
3314
3463
  }
3315
3464
  }
3465
+ if ("dangerous" in obj && obj["dangerous"] !== void 0) {
3466
+ if (typeof obj["dangerous"] !== "boolean") {
3467
+ errors.push({
3468
+ stepIndex: i,
3469
+ field: "dangerous",
3470
+ message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
3471
+ });
3472
+ }
3473
+ }
3474
+ for (const condField of ["expectAny", "expectAll", "failIf"]) {
3475
+ if (condField in obj && obj[condField] !== void 0) {
3476
+ if (!Array.isArray(obj[condField])) {
3477
+ errors.push({
3478
+ stepIndex: i,
3479
+ field: condField,
3480
+ message: `"${condField}" expected array, got ${typeof obj[condField]}.`
3481
+ });
3482
+ } else {
3483
+ const conditions = obj[condField];
3484
+ for (let ci = 0; ci < conditions.length; ci++) {
3485
+ const cond = conditions[ci];
3486
+ if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
3487
+ errors.push({
3488
+ stepIndex: i,
3489
+ field: condField,
3490
+ message: `"${condField}[${ci}]" must be a condition object.`
3491
+ });
3492
+ continue;
3493
+ }
3494
+ const condObj = cond;
3495
+ if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
3496
+ errors.push({
3497
+ stepIndex: i,
3498
+ field: condField,
3499
+ message: `"${condField}[${ci}]" missing required "kind" field.`
3500
+ });
3501
+ } else {
3502
+ const validKinds = [
3503
+ "urlMatches",
3504
+ "elementVisible",
3505
+ "elementHidden",
3506
+ "textAppears",
3507
+ "textChanges",
3508
+ "networkResponse",
3509
+ "stateSignatureChanges"
3510
+ ];
3511
+ if (!validKinds.includes(condObj["kind"])) {
3512
+ errors.push({
3513
+ stepIndex: i,
3514
+ field: condField,
3515
+ message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
3516
+ });
3517
+ }
3518
+ }
3519
+ }
3520
+ }
3521
+ }
3522
+ }
3316
3523
  if (action === "assertText") {
3317
3524
  if (!("expect" in obj) && !("value" in obj)) {
3318
3525
  errors.push({
@@ -5373,6 +5580,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
5373
5580
  });
5374
5581
  }
5375
5582
 
5583
+ // src/browser/delta.ts
5584
+ function extractPageState(url, title, snapshot, forms, pageText) {
5585
+ const headings = [];
5586
+ const buttons = [];
5587
+ const alerts = [];
5588
+ function walkNodes(nodes) {
5589
+ for (const node of nodes) {
5590
+ const role = node.role?.toLowerCase() ?? "";
5591
+ if (role === "heading" && node.name) {
5592
+ headings.push(node.name);
5593
+ }
5594
+ if ((role === "button" || role === "link") && node.name) {
5595
+ const disabled = node.disabled ?? false;
5596
+ buttons.push({ text: node.name, disabled, ref: node.ref });
5597
+ }
5598
+ if (role === "alert" && node.name) {
5599
+ alerts.push(node.name);
5600
+ }
5601
+ if (node.children) {
5602
+ walkNodes(node.children);
5603
+ }
5604
+ }
5605
+ }
5606
+ walkNodes(snapshot.accessibilityTree);
5607
+ const formFields = forms.map((f) => ({
5608
+ label: f.label,
5609
+ name: f.name,
5610
+ id: f.id,
5611
+ value: f.value,
5612
+ type: f.type
5613
+ }));
5614
+ return {
5615
+ url,
5616
+ title,
5617
+ headings,
5618
+ formFields,
5619
+ buttons,
5620
+ alerts,
5621
+ visibleText: pageText.slice(0, 3e3)
5622
+ };
5623
+ }
5624
+ function computeDelta(before, after) {
5625
+ const changes = [];
5626
+ if (before.url !== after.url) {
5627
+ changes.push({ kind: "url", before: before.url, after: after.url });
5628
+ }
5629
+ if (before.title !== after.title) {
5630
+ changes.push({ kind: "title", before: before.title, after: after.title });
5631
+ }
5632
+ const beforeHeadings = new Set(before.headings);
5633
+ const afterHeadings = new Set(after.headings);
5634
+ for (const h of after.headings) {
5635
+ if (!beforeHeadings.has(h)) {
5636
+ changes.push({ kind: "heading_added", after: h });
5637
+ }
5638
+ }
5639
+ for (const h of before.headings) {
5640
+ if (!afterHeadings.has(h)) {
5641
+ changes.push({ kind: "heading_removed", before: h });
5642
+ }
5643
+ }
5644
+ const beforeFieldMap = new Map(
5645
+ before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
5646
+ );
5647
+ for (const af of after.formFields) {
5648
+ const key = af.id ?? af.name ?? af.label ?? "";
5649
+ const bf = beforeFieldMap.get(key);
5650
+ if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
5651
+ changes.push({
5652
+ kind: "field_changed",
5653
+ before: String(bf.value ?? ""),
5654
+ after: String(af.value ?? ""),
5655
+ detail: af.label ?? af.name ?? af.id ?? key
5656
+ });
5657
+ }
5658
+ }
5659
+ const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
5660
+ for (const ab of after.buttons) {
5661
+ const bb = beforeBtnMap.get(ab.text);
5662
+ if (bb && bb.disabled !== ab.disabled) {
5663
+ changes.push({
5664
+ kind: "button_changed",
5665
+ detail: ab.text,
5666
+ before: bb.disabled ? "disabled" : "enabled",
5667
+ after: ab.disabled ? "disabled" : "enabled"
5668
+ });
5669
+ }
5670
+ }
5671
+ const beforeAlerts = new Set(before.alerts);
5672
+ const afterAlerts = new Set(after.alerts);
5673
+ for (const a of after.alerts) {
5674
+ if (!beforeAlerts.has(a)) {
5675
+ changes.push({ kind: "alert_added", after: a });
5676
+ }
5677
+ }
5678
+ for (const a of before.alerts) {
5679
+ if (!afterAlerts.has(a)) {
5680
+ changes.push({ kind: "alert_removed", before: a });
5681
+ }
5682
+ }
5683
+ return {
5684
+ changes,
5685
+ before,
5686
+ after,
5687
+ hasChanges: changes.length > 0
5688
+ };
5689
+ }
5690
+
5376
5691
  // src/browser/keyboard.ts
5377
5692
  var US_KEYBOARD = {
5378
5693
  // Letters (lowercase)
@@ -5532,8 +5847,118 @@ function parseShortcut(combo) {
5532
5847
  return { modifiers, key };
5533
5848
  }
5534
5849
 
5850
+ // src/browser/review.ts
5851
+ function extractReview(url, title, snapshot, forms, pageText) {
5852
+ const headings = [];
5853
+ const alerts = [];
5854
+ const statusLabels = [];
5855
+ const keyValues = [];
5856
+ const tables = [];
5857
+ const summaryCards = [];
5858
+ function walkNodes(nodes, parentHeading) {
5859
+ let currentHeading = parentHeading;
5860
+ for (const node of nodes) {
5861
+ const role = node.role?.toLowerCase() ?? "";
5862
+ if (role === "heading" && node.name) {
5863
+ headings.push(node.name);
5864
+ currentHeading = node.name;
5865
+ }
5866
+ if (role === "alert" && node.name) {
5867
+ alerts.push(node.name);
5868
+ }
5869
+ if (role === "status" && node.name) {
5870
+ statusLabels.push(node.name);
5871
+ }
5872
+ if (role === "table" || role === "grid") {
5873
+ const table = extractTableFromNode(node);
5874
+ if (table) tables.push(table);
5875
+ }
5876
+ if ((role === "definition" || role === "term") && node.name) {
5877
+ if (role === "term") {
5878
+ keyValues.push({ key: node.name, value: "" });
5879
+ } else if (role === "definition" && keyValues.length > 0) {
5880
+ const last = keyValues[keyValues.length - 1];
5881
+ if (!last.value) last.value = node.name;
5882
+ }
5883
+ }
5884
+ if (node.children) {
5885
+ walkNodes(node.children, currentHeading);
5886
+ }
5887
+ }
5888
+ }
5889
+ walkNodes(snapshot.accessibilityTree);
5890
+ const textKvPairs = extractKeyValueFromText(pageText);
5891
+ keyValues.push(...textKvPairs);
5892
+ const formEntries = forms.map((f) => ({
5893
+ label: f.label,
5894
+ value: f.value,
5895
+ type: f.type,
5896
+ disabled: f.disabled
5897
+ }));
5898
+ return {
5899
+ url,
5900
+ title,
5901
+ headings,
5902
+ forms: formEntries,
5903
+ alerts,
5904
+ summaryCards,
5905
+ tables,
5906
+ keyValues,
5907
+ statusLabels
5908
+ };
5909
+ }
5910
+ function extractTableFromNode(node) {
5911
+ const headers = [];
5912
+ const rows = [];
5913
+ function findRows(n) {
5914
+ const role = n.role?.toLowerCase() ?? "";
5915
+ if (role === "columnheader" && n.name) {
5916
+ headers.push(n.name);
5917
+ }
5918
+ if (role === "row") {
5919
+ const cells = [];
5920
+ if (n.children) {
5921
+ for (const child of n.children) {
5922
+ const childRole = child.role?.toLowerCase() ?? "";
5923
+ if ((childRole === "cell" || childRole === "gridcell") && child.name) {
5924
+ cells.push(child.name);
5925
+ }
5926
+ }
5927
+ }
5928
+ if (cells.length > 0) rows.push(cells);
5929
+ }
5930
+ if (n.children) {
5931
+ for (const child of n.children) findRows(child);
5932
+ }
5933
+ }
5934
+ findRows(node);
5935
+ if (rows.length === 0) return null;
5936
+ return { headers, rows };
5937
+ }
5938
+ function extractKeyValueFromText(text) {
5939
+ const pairs = [];
5940
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
5941
+ for (const line of lines) {
5942
+ const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
5943
+ if (match) {
5944
+ pairs.push({ key: match[1].trim(), value: match[2].trim() });
5945
+ }
5946
+ }
5947
+ return pairs.slice(0, 20);
5948
+ }
5949
+
5535
5950
  // src/browser/page.ts
5536
5951
  var DEFAULT_TIMEOUT2 = 3e4;
5952
+ function normalizeAXCheckedValue(value) {
5953
+ if (typeof value === "boolean") {
5954
+ return value;
5955
+ }
5956
+ if (typeof value === "string") {
5957
+ if (value === "true") return true;
5958
+ if (value === "false") return false;
5959
+ }
5960
+ return void 0;
5961
+ }
5537
5962
  var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
5538
5963
  if (globalThis.__bpEventListenerTrackerInstalled) return;
5539
5964
  Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
@@ -7319,7 +7744,9 @@ var Page = class {
7319
7744
  }
7320
7745
  }
7321
7746
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7322
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
7747
+ const checked = normalizeAXCheckedValue(
7748
+ node.properties?.find((p) => p.name === "checked")?.value.value
7749
+ );
7323
7750
  return {
7324
7751
  role,
7325
7752
  name,
@@ -7375,7 +7802,9 @@ var Page = class {
7375
7802
  const ref = nodeRefs.get(node.nodeId);
7376
7803
  const name = node.name?.value ?? "";
7377
7804
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
7378
- const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
7805
+ const checked = normalizeAXCheckedValue(
7806
+ node.properties?.find((p) => p.name === "checked")?.value.value
7807
+ );
7379
7808
  const value = node.value?.value;
7380
7809
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
7381
7810
  interactiveElements.push({
@@ -7442,6 +7871,45 @@ var Page = class {
7442
7871
  }
7443
7872
  }
7444
7873
  }
7874
+ // ============ Delta & Review ============
7875
+ /**
7876
+ * Capture current page state for delta comparison.
7877
+ * Call before an action, then call delta() again after and use computeDelta().
7878
+ */
7879
+ async captureState() {
7880
+ const [url, title, snapshot, forms, text] = await Promise.all([
7881
+ this.url(),
7882
+ this.title(),
7883
+ this.snapshot(),
7884
+ this.forms(),
7885
+ this.text()
7886
+ ]);
7887
+ return extractPageState(url, title, snapshot, forms, text);
7888
+ }
7889
+ /**
7890
+ * Compute what changed between two page states.
7891
+ * If no arguments: captures current state and returns it (for use as "before").
7892
+ * If one argument (before state): captures current state and computes delta.
7893
+ */
7894
+ async delta(before) {
7895
+ const currentState = await this.captureState();
7896
+ if (!before) return currentState;
7897
+ return computeDelta(before, currentState);
7898
+ }
7899
+ /**
7900
+ * Extract structured review surface from the current page.
7901
+ * Returns headings, form values, alerts, key-value pairs, tables, and status labels.
7902
+ */
7903
+ async review() {
7904
+ const [url, title, snapshot, forms, text] = await Promise.all([
7905
+ this.url(),
7906
+ this.title(),
7907
+ this.snapshot(),
7908
+ this.forms(),
7909
+ this.text()
7910
+ ]);
7911
+ return extractReview(url, title, snapshot, forms, text);
7912
+ }
7445
7913
  // ============ Batch Execution ============
7446
7914
  /**
7447
7915
  * Execute a batch of steps
@@ -8620,6 +9088,10 @@ function sleep4(ms) {
8620
9088
 
8621
9089
  export {
8622
9090
  pcmToWav,
9091
+ readString,
9092
+ readStringOr,
9093
+ formatConsoleArg,
9094
+ globToRegex,
8623
9095
  SENSITIVE_AUTOCOMPLETE_TOKENS,
8624
9096
  redactValueForRecording,
8625
9097
  fuzzyMatchElements,
@@ -8627,11 +9099,10 @@ export {
8627
9099
  buildTraceSummaries,
8628
9100
  createRecordingManifest,
8629
9101
  canonicalizeRecordingArtifact,
8630
- TRACE_BINDING_NAME,
8631
- TRACE_SCRIPT,
8632
9102
  createTraceId,
8633
9103
  normalizeTraceEvent,
8634
- LiveTraceCollector,
9104
+ TRACE_BINDING_NAME,
9105
+ TRACE_SCRIPT,
8635
9106
  addBatchToPage,
8636
9107
  validateSteps,
8637
9108
  grantAudioPermissions,