libretto 0.4.4 → 0.5.1

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 (194) hide show
  1. package/README.md +106 -36
  2. package/dist/cli/cli.js +39 -113
  3. package/dist/cli/commands/ai.js +1 -1
  4. package/dist/cli/commands/browser.js +87 -60
  5. package/dist/cli/commands/execution.js +201 -88
  6. package/dist/cli/commands/init.js +30 -8
  7. package/dist/cli/commands/logs.js +5 -6
  8. package/dist/cli/commands/shared.js +30 -29
  9. package/dist/cli/commands/snapshot.js +26 -39
  10. package/dist/cli/core/ai-config.js +9 -2
  11. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  12. package/dist/cli/core/browser.js +141 -33
  13. package/dist/cli/core/context.js +7 -18
  14. package/dist/cli/core/session-telemetry.js +5 -2
  15. package/dist/cli/core/session.js +23 -10
  16. package/dist/cli/core/snapshot-analyzer.js +16 -33
  17. package/dist/cli/core/snapshot-api-config.js +2 -6
  18. package/dist/cli/core/telemetry.js +10 -2
  19. package/dist/cli/framework/simple-cli.js +45 -25
  20. package/dist/cli/router.js +14 -21
  21. package/dist/cli/workers/run-integration-runtime.js +26 -7
  22. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  23. package/dist/cli/workers/run-integration-worker.js +1 -4
  24. package/dist/index.d.ts +1 -2
  25. package/dist/index.js +7 -10
  26. package/dist/runtime/download/download.js +5 -1
  27. package/dist/runtime/extract/extract.js +11 -2
  28. package/dist/runtime/network/network.js +8 -1
  29. package/dist/runtime/recovery/agent.js +6 -2
  30. package/dist/runtime/recovery/errors.js +3 -1
  31. package/dist/runtime/recovery/recovery.js +3 -1
  32. package/dist/shared/condense-dom/condense-dom.js +6 -13
  33. package/dist/shared/config/config.d.ts +1 -9
  34. package/dist/shared/config/config.js +0 -18
  35. package/dist/shared/config/index.d.ts +2 -1
  36. package/dist/shared/config/index.js +0 -10
  37. package/dist/shared/debug/pause.js +9 -3
  38. package/dist/shared/instrumentation/instrument.js +101 -5
  39. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  40. package/dist/shared/llm/client.js +3 -1
  41. package/dist/shared/logger/index.js +4 -1
  42. package/dist/shared/paths/paths.js +2 -1
  43. package/dist/shared/paths/repo-root.d.ts +3 -0
  44. package/dist/shared/paths/repo-root.js +24 -0
  45. package/dist/shared/run/api.js +3 -1
  46. package/dist/shared/run/browser.js +7 -2
  47. package/dist/shared/state/session-state.d.ts +2 -1
  48. package/dist/shared/state/session-state.js +5 -2
  49. package/dist/shared/visualization/ghost-cursor.js +19 -10
  50. package/dist/shared/visualization/highlight.js +9 -6
  51. package/dist/shared/workflow/workflow.d.ts +4 -5
  52. package/dist/shared/workflow/workflow.js +3 -5
  53. package/package.json +11 -8
  54. package/scripts/check-skills-sync.mjs +25 -0
  55. package/scripts/compare-eval-summary.mjs +47 -0
  56. package/scripts/postinstall.mjs +26 -17
  57. package/scripts/prepare-release.sh +97 -0
  58. package/scripts/skills-libretto.mjs +103 -0
  59. package/scripts/summarize-evals.mjs +135 -0
  60. package/scripts/sync-skills.mjs +12 -0
  61. package/skills/libretto/SKILL.md +130 -377
  62. package/skills/libretto/references/auth-profiles.md +30 -0
  63. package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
  64. package/skills/libretto/references/configuration-file-reference.md +53 -0
  65. package/skills/libretto/references/pages-and-page-targeting.md +29 -0
  66. package/skills/libretto/references/site-security-review.md +143 -0
  67. package/src/cli/cli.ts +86 -0
  68. package/src/cli/commands/ai.ts +35 -0
  69. package/src/cli/commands/browser.ts +189 -0
  70. package/src/cli/commands/execution.ts +822 -0
  71. package/src/cli/commands/init.ts +350 -0
  72. package/src/cli/commands/logs.ts +128 -0
  73. package/src/cli/commands/shared.ts +69 -0
  74. package/src/cli/commands/snapshot.ts +312 -0
  75. package/src/cli/core/ai-config.ts +264 -0
  76. package/src/cli/core/api-snapshot-analyzer.ts +108 -0
  77. package/src/cli/core/browser.ts +976 -0
  78. package/src/cli/core/context.ts +127 -0
  79. package/src/cli/core/pause-signals.ts +35 -0
  80. package/src/cli/core/session-telemetry.ts +564 -0
  81. package/src/cli/core/session.ts +223 -0
  82. package/src/cli/core/snapshot-analyzer.ts +855 -0
  83. package/src/cli/core/snapshot-api-config.ts +231 -0
  84. package/src/cli/core/telemetry.ts +459 -0
  85. package/src/cli/framework/simple-cli.ts +1340 -0
  86. package/src/cli/index.ts +13 -0
  87. package/src/cli/router.ts +20 -0
  88. package/src/cli/workers/run-integration-runtime.ts +338 -0
  89. package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
  90. package/src/cli/workers/run-integration-worker.ts +72 -0
  91. package/src/index.ts +127 -0
  92. package/src/runtime/download/download.ts +104 -0
  93. package/src/runtime/download/index.ts +7 -0
  94. package/src/runtime/extract/extract.ts +102 -0
  95. package/src/runtime/extract/index.ts +1 -0
  96. package/src/runtime/network/index.ts +5 -0
  97. package/src/runtime/network/network.ts +119 -0
  98. package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
  99. package/src/runtime/recovery/errors.ts +155 -0
  100. package/src/runtime/recovery/index.ts +7 -0
  101. package/src/runtime/recovery/recovery.ts +53 -0
  102. package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
  103. package/src/shared/config/config.ts +3 -0
  104. package/src/shared/config/index.ts +0 -0
  105. package/src/shared/debug/index.ts +1 -0
  106. package/src/shared/debug/pause.ts +91 -0
  107. package/src/shared/instrumentation/errors.ts +84 -0
  108. package/src/shared/instrumentation/index.ts +9 -0
  109. package/src/shared/instrumentation/instrument.ts +406 -0
  110. package/src/shared/llm/ai-sdk-adapter.ts +81 -0
  111. package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
  112. package/src/shared/llm/index.ts +3 -0
  113. package/src/shared/llm/types.ts +63 -0
  114. package/src/shared/logger/index.ts +13 -0
  115. package/src/shared/logger/logger.ts +358 -0
  116. package/src/shared/logger/sinks.ts +148 -0
  117. package/src/shared/paths/paths.ts +110 -0
  118. package/src/shared/paths/repo-root.ts +27 -0
  119. package/src/shared/run/api.ts +6 -0
  120. package/src/shared/run/browser.ts +107 -0
  121. package/src/shared/state/index.ts +11 -0
  122. package/src/shared/state/session-state.ts +77 -0
  123. package/src/shared/visualization/ghost-cursor.ts +213 -0
  124. package/src/shared/visualization/highlight.ts +149 -0
  125. package/src/shared/visualization/index.ts +18 -0
  126. package/src/shared/workflow/workflow.ts +36 -0
  127. package/dist/index.cjs +0 -144
  128. package/dist/index.d.cts +0 -21
  129. package/dist/runtime/download/download.cjs +0 -70
  130. package/dist/runtime/download/download.d.cts +0 -35
  131. package/dist/runtime/download/index.cjs +0 -30
  132. package/dist/runtime/download/index.d.cts +0 -3
  133. package/dist/runtime/extract/extract.cjs +0 -88
  134. package/dist/runtime/extract/extract.d.cts +0 -23
  135. package/dist/runtime/extract/index.cjs +0 -28
  136. package/dist/runtime/extract/index.d.cts +0 -5
  137. package/dist/runtime/network/index.cjs +0 -28
  138. package/dist/runtime/network/index.d.cts +0 -4
  139. package/dist/runtime/network/network.cjs +0 -91
  140. package/dist/runtime/network/network.d.cts +0 -28
  141. package/dist/runtime/recovery/agent.d.cts +0 -13
  142. package/dist/runtime/recovery/errors.cjs +0 -124
  143. package/dist/runtime/recovery/errors.d.cts +0 -31
  144. package/dist/runtime/recovery/index.cjs +0 -34
  145. package/dist/runtime/recovery/index.d.cts +0 -7
  146. package/dist/runtime/recovery/recovery.cjs +0 -55
  147. package/dist/runtime/recovery/recovery.d.cts +0 -12
  148. package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
  149. package/dist/shared/config/config.cjs +0 -44
  150. package/dist/shared/config/config.d.cts +0 -10
  151. package/dist/shared/config/index.cjs +0 -32
  152. package/dist/shared/config/index.d.cts +0 -1
  153. package/dist/shared/debug/index.cjs +0 -28
  154. package/dist/shared/debug/index.d.cts +0 -1
  155. package/dist/shared/debug/pause.cjs +0 -86
  156. package/dist/shared/debug/pause.d.cts +0 -12
  157. package/dist/shared/instrumentation/errors.cjs +0 -81
  158. package/dist/shared/instrumentation/errors.d.cts +0 -12
  159. package/dist/shared/instrumentation/index.cjs +0 -35
  160. package/dist/shared/instrumentation/index.d.cts +0 -6
  161. package/dist/shared/instrumentation/instrument.cjs +0 -206
  162. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  163. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
  164. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  165. package/dist/shared/llm/client.d.cts +0 -13
  166. package/dist/shared/llm/index.cjs +0 -31
  167. package/dist/shared/llm/index.d.cts +0 -5
  168. package/dist/shared/llm/types.cjs +0 -16
  169. package/dist/shared/llm/types.d.cts +0 -67
  170. package/dist/shared/logger/index.cjs +0 -37
  171. package/dist/shared/logger/index.d.cts +0 -2
  172. package/dist/shared/logger/logger.cjs +0 -232
  173. package/dist/shared/logger/logger.d.cts +0 -86
  174. package/dist/shared/logger/sinks.cjs +0 -160
  175. package/dist/shared/logger/sinks.d.cts +0 -9
  176. package/dist/shared/paths/paths.cjs +0 -104
  177. package/dist/shared/paths/paths.d.cts +0 -10
  178. package/dist/shared/run/api.cjs +0 -28
  179. package/dist/shared/run/api.d.cts +0 -2
  180. package/dist/shared/run/browser.cjs +0 -98
  181. package/dist/shared/run/browser.d.cts +0 -22
  182. package/dist/shared/state/index.cjs +0 -38
  183. package/dist/shared/state/index.d.cts +0 -2
  184. package/dist/shared/state/session-state.cjs +0 -92
  185. package/dist/shared/state/session-state.d.cts +0 -40
  186. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  187. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  188. package/dist/shared/visualization/highlight.cjs +0 -134
  189. package/dist/shared/visualization/highlight.d.cts +0 -22
  190. package/dist/shared/visualization/index.cjs +0 -45
  191. package/dist/shared/visualization/index.d.cts +0 -3
  192. package/dist/shared/workflow/workflow.cjs +0 -47
  193. package/dist/shared/workflow/workflow.d.cts +0 -21
  194. package/skills/libretto/integration-approach-selection.md +0 -174
@@ -0,0 +1,564 @@
1
+ import type { BrowserContext, Page } from "playwright";
2
+
3
+ type TelemetryEntry = Record<string, unknown>;
4
+
5
+ type InstallSessionTelemetryOptions = {
6
+ context: BrowserContext;
7
+ initialPage: Page;
8
+ logAction: (entry: TelemetryEntry) => void;
9
+ logNetwork: (entry: TelemetryEntry) => void;
10
+ includeUserDomActions?: boolean;
11
+ };
12
+
13
+ export async function installSessionTelemetry(
14
+ options: InstallSessionTelemetryOptions,
15
+ ): Promise<void> {
16
+ const STATIC_EXT_RE =
17
+ /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
18
+ const { context, initialPage, logAction, logNetwork } = options;
19
+ const includeUserDomActions = options.includeUserDomActions ?? false;
20
+ const pageIdCache = new WeakMap<Page, string>();
21
+ const wrappedPages = new WeakSet<Page>();
22
+ const exposedPages = new WeakSet<Page>();
23
+
24
+ const resolvePageId = async (page: Page): Promise<string> => {
25
+ if (pageIdCache.has(page)) return pageIdCache.get(page)!;
26
+ const cdpSession = await context.newCDPSession(page);
27
+ try {
28
+ const targetInfo = await cdpSession.send("Target.getTargetInfo");
29
+ const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
30
+ ?.targetInfo?.targetId;
31
+ if (typeof targetId !== "string" || targetId.length === 0) {
32
+ throw new Error(
33
+ `Could not resolve target id for page at URL "${page.url()}".`,
34
+ );
35
+ }
36
+ pageIdCache.set(page, targetId);
37
+ return targetId;
38
+ } finally {
39
+ await cdpSession.detach();
40
+ }
41
+ };
42
+
43
+ const emitAction = (entry: TelemetryEntry): void => {
44
+ logAction({
45
+ ts: new Date().toISOString(),
46
+ ...entry,
47
+ });
48
+ };
49
+
50
+ const emitNetwork = (entry: TelemetryEntry): void => {
51
+ logNetwork({
52
+ ts: new Date().toISOString(),
53
+ ...entry,
54
+ });
55
+ };
56
+
57
+ const markApiActionInProgress = async (
58
+ page: Page,
59
+ inProgress: boolean,
60
+ ): Promise<void> => {
61
+ await page.evaluate((flag) => {
62
+ (window as any).__btApiActionInProgress = flag;
63
+ }, inProgress);
64
+ };
65
+
66
+ const wrapLocator = (locator: any, page: Page, pageId: string): any => {
67
+ if (locator.__librettoActionLogged) return locator;
68
+ locator.__librettoActionLogged = true;
69
+
70
+ const locatorActionMethods = [
71
+ "click",
72
+ "dblclick",
73
+ "fill",
74
+ "type",
75
+ "press",
76
+ "check",
77
+ "uncheck",
78
+ "selectOption",
79
+ "hover",
80
+ "focus",
81
+ "scrollIntoViewIfNeeded",
82
+ "waitFor",
83
+ "innerHTML",
84
+ "innerText",
85
+ "textContent",
86
+ "inputValue",
87
+ "isChecked",
88
+ "isDisabled",
89
+ "isEditable",
90
+ "isEnabled",
91
+ "isHidden",
92
+ "isVisible",
93
+ "count",
94
+ "boundingBox",
95
+ "screenshot",
96
+ "evaluate",
97
+ "evaluateAll",
98
+ "evaluateHandle",
99
+ "getAttribute",
100
+ "dispatchEvent",
101
+ "setInputFiles",
102
+ "selectText",
103
+ "dragTo",
104
+ "highlight",
105
+ "tap",
106
+ ] as const;
107
+ const locatorReturningMethods = [
108
+ "first",
109
+ "last",
110
+ "locator",
111
+ "getByRole",
112
+ "getByText",
113
+ "getByLabel",
114
+ "getByPlaceholder",
115
+ "getByAltText",
116
+ "getByTitle",
117
+ "getByTestId",
118
+ "filter",
119
+ "and",
120
+ "or",
121
+ ] as const;
122
+
123
+ for (const actMethod of locatorActionMethods) {
124
+ if (typeof locator[actMethod] !== "function") continue;
125
+ const originalAction = locator[actMethod].bind(locator);
126
+ locator[actMethod] = async (...actionArgs: any[]) => {
127
+ const start = Date.now();
128
+ await markApiActionInProgress(page, true);
129
+ try {
130
+ const result = await originalAction(...actionArgs);
131
+ emitAction({
132
+ pageId,
133
+ action: actMethod,
134
+ source: "agent",
135
+ selector: "locator",
136
+ value:
137
+ actionArgs[0] !== undefined
138
+ ? String(actionArgs[0]).slice(0, 100)
139
+ : undefined,
140
+ duration: Date.now() - start,
141
+ success: true,
142
+ });
143
+ return result;
144
+ } catch (error: any) {
145
+ emitAction({
146
+ pageId,
147
+ action: actMethod,
148
+ source: "agent",
149
+ selector: "locator",
150
+ duration: Date.now() - start,
151
+ success: false,
152
+ error: error?.message ?? String(error),
153
+ });
154
+ throw error;
155
+ } finally {
156
+ await markApiActionInProgress(page, false);
157
+ }
158
+ };
159
+ }
160
+
161
+ for (const method of locatorReturningMethods) {
162
+ if (typeof locator[method] !== "function") continue;
163
+ const originalMethod = locator[method].bind(locator);
164
+ locator[method] = (...args: any[]) => {
165
+ const child = originalMethod(...args);
166
+ return wrapLocator(child, page, pageId);
167
+ };
168
+ }
169
+
170
+ if (typeof locator.nth === "function") {
171
+ const originalNth = locator.nth.bind(locator);
172
+ locator.nth = (index: number) => {
173
+ const child = originalNth(index);
174
+ return wrapLocator(child, page, pageId);
175
+ };
176
+ }
177
+
178
+ if (typeof locator.all === "function") {
179
+ const originalAll = locator.all.bind(locator);
180
+ locator.all = async () => {
181
+ const items: any[] = await originalAll();
182
+ return items.map((item: any) => wrapLocator(item, page, pageId));
183
+ };
184
+ }
185
+
186
+ return locator;
187
+ };
188
+
189
+ const installUserDomTracking = async (
190
+ page: Page,
191
+ pageId: string,
192
+ ): Promise<void> => {
193
+ if (exposedPages.has(page)) return;
194
+ exposedPages.add(page);
195
+
196
+ await page.exposeFunction("__btActionLog", (jsonStr: string) => {
197
+ const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
198
+ emitAction({
199
+ pageId,
200
+ source: "user",
201
+ ...parsed,
202
+ });
203
+ });
204
+
205
+ await page.addInitScript(() => {
206
+ if ((window as any).__btDomListenersInstalled) return;
207
+ (window as any).__btDomListenersInstalled = true;
208
+
209
+ const identify = (el: any): string => {
210
+ if (!el || !el.tagName) return "";
211
+ const testId = el.getAttribute("data-testid");
212
+ if (testId) return `[data-testid="${testId}"]`;
213
+ const role = el.getAttribute("role") || "";
214
+ const id = el.id;
215
+ if (role && id) return `${role}#${id}`;
216
+ const label =
217
+ el.getAttribute("aria-label") ||
218
+ (el.textContent || "").trim().slice(0, 30) ||
219
+ "";
220
+ if (role && label) return `${role} "${label}"`;
221
+ const tag = el.tagName.toLowerCase();
222
+ const cls =
223
+ el.className && typeof el.className === "string"
224
+ ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".")
225
+ : "";
226
+ return `${tag}${cls}`;
227
+ };
228
+
229
+ let clickTimer: ReturnType<typeof setTimeout> | null = null;
230
+ let pendingClick: { selector: string } | null = null;
231
+
232
+ document.addEventListener(
233
+ "click",
234
+ (event) => {
235
+ if ((window as any).__btApiActionInProgress) return;
236
+ const target = event.target as any;
237
+ const selector = identify(target);
238
+ if (target?.type === "checkbox") {
239
+ (window as any).__btActionLog(
240
+ JSON.stringify({
241
+ action: target.checked ? "check" : "uncheck",
242
+ selector,
243
+ success: true,
244
+ }),
245
+ );
246
+ return;
247
+ }
248
+ pendingClick = { selector };
249
+ if (clickTimer) clearTimeout(clickTimer);
250
+ clickTimer = setTimeout(() => {
251
+ if (pendingClick) {
252
+ (window as any).__btActionLog(
253
+ JSON.stringify({
254
+ action: "click",
255
+ selector: pendingClick.selector,
256
+ success: true,
257
+ }),
258
+ );
259
+ }
260
+ pendingClick = null;
261
+ clickTimer = null;
262
+ }, 200);
263
+ },
264
+ true,
265
+ );
266
+
267
+ document.addEventListener(
268
+ "dblclick",
269
+ (event) => {
270
+ if ((window as any).__btApiActionInProgress) return;
271
+ if (clickTimer) {
272
+ clearTimeout(clickTimer);
273
+ clickTimer = null;
274
+ pendingClick = null;
275
+ }
276
+ const selector = identify(event.target);
277
+ (window as any).__btActionLog(
278
+ JSON.stringify({ action: "dblclick", selector, success: true }),
279
+ );
280
+ },
281
+ true,
282
+ );
283
+
284
+ const inputTimers = new WeakMap<any, ReturnType<typeof setTimeout>>();
285
+ document.addEventListener(
286
+ "input",
287
+ (event) => {
288
+ if ((window as any).__btApiActionInProgress) return;
289
+ const target = event.target as any;
290
+ const selector = identify(target);
291
+ if (target.tagName === "SELECT") {
292
+ (window as any).__btActionLog(
293
+ JSON.stringify({
294
+ action: "selectOption",
295
+ selector,
296
+ value: target.value,
297
+ success: true,
298
+ }),
299
+ );
300
+ return;
301
+ }
302
+ const existing = inputTimers.get(target);
303
+ if (existing) clearTimeout(existing);
304
+ inputTimers.set(
305
+ target,
306
+ setTimeout(() => {
307
+ inputTimers.delete(target);
308
+ (window as any).__btActionLog(
309
+ JSON.stringify({
310
+ action: "fill",
311
+ selector,
312
+ value: (target.value || "").slice(0, 100),
313
+ success: true,
314
+ }),
315
+ );
316
+ }, 500),
317
+ );
318
+ },
319
+ true,
320
+ );
321
+
322
+ const specialKeys = new Set([
323
+ "Enter",
324
+ "Escape",
325
+ "Tab",
326
+ "Backspace",
327
+ "Delete",
328
+ "ArrowUp",
329
+ "ArrowDown",
330
+ "ArrowLeft",
331
+ "ArrowRight",
332
+ "Home",
333
+ "End",
334
+ "PageUp",
335
+ "PageDown",
336
+ "F1",
337
+ "F2",
338
+ "F3",
339
+ "F4",
340
+ "F5",
341
+ "F6",
342
+ "F7",
343
+ "F8",
344
+ "F9",
345
+ "F10",
346
+ "F11",
347
+ "F12",
348
+ ]);
349
+ document.addEventListener(
350
+ "keydown",
351
+ (event) => {
352
+ if ((window as any).__btApiActionInProgress) return;
353
+ const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
354
+ if (!isShortcut && !specialKeys.has(event.key)) return;
355
+ const selector = identify(event.target);
356
+ const keyDesc =
357
+ (event.ctrlKey ? "Ctrl+" : "") +
358
+ (event.metaKey ? "Meta+" : "") +
359
+ (event.altKey ? "Alt+" : "") +
360
+ (event.shiftKey ? "Shift+" : "") +
361
+ event.key;
362
+ (window as any).__btActionLog(
363
+ JSON.stringify({
364
+ action: "press",
365
+ selector,
366
+ value: keyDesc,
367
+ success: true,
368
+ }),
369
+ );
370
+ },
371
+ true,
372
+ );
373
+
374
+ let scrollTimer: ReturnType<typeof setTimeout> | null = null;
375
+ document.addEventListener(
376
+ "scroll",
377
+ () => {
378
+ if ((window as any).__btApiActionInProgress) return;
379
+ if (scrollTimer) clearTimeout(scrollTimer);
380
+ scrollTimer = setTimeout(() => {
381
+ scrollTimer = null;
382
+ (window as any).__btActionLog(
383
+ JSON.stringify({
384
+ action: "scroll",
385
+ selector: "document",
386
+ value: `y=${window.scrollY}`,
387
+ success: true,
388
+ }),
389
+ );
390
+ }, 300);
391
+ },
392
+ true,
393
+ );
394
+ });
395
+ };
396
+
397
+ const wrapPageActions = (page: Page, pageId: string): void => {
398
+ if (wrappedPages.has(page)) return;
399
+ wrappedPages.add(page);
400
+
401
+ const pageActions = [
402
+ "click",
403
+ "dblclick",
404
+ "fill",
405
+ "type",
406
+ "press",
407
+ "check",
408
+ "uncheck",
409
+ "selectOption",
410
+ "hover",
411
+ "focus",
412
+ ] as const;
413
+ const navActions = ["goto", "reload", "goBack", "goForward"] as const;
414
+ const locatorFactories = [
415
+ "locator",
416
+ "getByRole",
417
+ "getByText",
418
+ "getByLabel",
419
+ "getByPlaceholder",
420
+ "getByAltText",
421
+ "getByTitle",
422
+ "getByTestId",
423
+ ] as const;
424
+
425
+ for (const method of pageActions) {
426
+ const originalMethod = (page as any)[method].bind(page);
427
+ (page as any)[method] = async (...args: any[]) => {
428
+ const start = Date.now();
429
+ await markApiActionInProgress(page, true);
430
+ try {
431
+ const result = await originalMethod(...args);
432
+ emitAction({
433
+ pageId,
434
+ action: method,
435
+ source: "agent",
436
+ selector: typeof args[0] === "string" ? args[0] : undefined,
437
+ value:
438
+ args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
439
+ duration: Date.now() - start,
440
+ success: true,
441
+ });
442
+ return result;
443
+ } catch (error: any) {
444
+ emitAction({
445
+ pageId,
446
+ action: method,
447
+ source: "agent",
448
+ selector: typeof args[0] === "string" ? args[0] : undefined,
449
+ duration: Date.now() - start,
450
+ success: false,
451
+ error: error?.message ?? String(error),
452
+ });
453
+ throw error;
454
+ } finally {
455
+ await markApiActionInProgress(page, false);
456
+ }
457
+ };
458
+ }
459
+
460
+ for (const method of navActions) {
461
+ const originalMethod = (page as any)[method].bind(page);
462
+ (page as any)[method] = async (...args: any[]) => {
463
+ const start = Date.now();
464
+ try {
465
+ const result = await originalMethod(...args);
466
+ emitAction({
467
+ pageId,
468
+ action: method,
469
+ source: "agent",
470
+ url: typeof args[0] === "string" ? args[0] : page.url(),
471
+ duration: Date.now() - start,
472
+ success: true,
473
+ });
474
+ return result;
475
+ } catch (error: any) {
476
+ emitAction({
477
+ pageId,
478
+ action: method,
479
+ source: "agent",
480
+ url: typeof args[0] === "string" ? args[0] : undefined,
481
+ duration: Date.now() - start,
482
+ success: false,
483
+ error: error?.message ?? String(error),
484
+ });
485
+ throw error;
486
+ }
487
+ };
488
+ }
489
+
490
+ for (const factory of locatorFactories) {
491
+ const originalFactory = (page as any)[factory].bind(page);
492
+ (page as any)[factory] = (...factoryArgs: any[]) => {
493
+ const locator = originalFactory(...factoryArgs);
494
+ return wrapLocator(locator, page, pageId);
495
+ };
496
+ }
497
+ };
498
+
499
+ const installForPage = async (page: Page): Promise<void> => {
500
+ const pageId = await resolvePageId(page);
501
+ wrapPageActions(page, pageId);
502
+
503
+ if (includeUserDomActions) {
504
+ await installUserDomTracking(page, pageId);
505
+ }
506
+
507
+ page.on("response", async (response) => {
508
+ const request = response.request();
509
+ const url = request.url();
510
+ if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
511
+ return;
512
+ emitNetwork({
513
+ pageId,
514
+ method: request.method(),
515
+ url,
516
+ status: response.status(),
517
+ contentType: response.headers()["content-type"] ?? null,
518
+ postData:
519
+ request.method() === "POST" ||
520
+ request.method() === "PUT" ||
521
+ request.method() === "PATCH"
522
+ ? (request.postData() ?? "").substring(0, 2000)
523
+ : undefined,
524
+ responseBody: null,
525
+ size: null,
526
+ durationMs: null,
527
+ });
528
+ });
529
+
530
+ page.on("framenavigated", (frame) => {
531
+ if (frame !== page.mainFrame()) return;
532
+ emitAction({
533
+ pageId,
534
+ action: "navigate",
535
+ source: "agent",
536
+ url: frame.url(),
537
+ success: true,
538
+ });
539
+ });
540
+ page.on("popup", (popup) => {
541
+ emitAction({
542
+ pageId,
543
+ action: "popup",
544
+ source: "agent",
545
+ url: popup.url(),
546
+ success: true,
547
+ });
548
+ });
549
+ page.on("dialog", (dialog) => {
550
+ emitAction({
551
+ pageId,
552
+ action: "dialog",
553
+ source: "agent",
554
+ value: `${dialog.type()}: ${dialog.message().slice(0, 500)}`,
555
+ success: true,
556
+ });
557
+ });
558
+ };
559
+
560
+ await installForPage(initialPage);
561
+ context.on("page", (newPage) => {
562
+ void installForPage(newPage);
563
+ });
564
+ }