libretto 0.5.0 → 0.5.2

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 (122) hide show
  1. package/README.md +109 -35
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +34 -29
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +21 -4
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +207 -37
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +434 -174
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +20 -4
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +17 -69
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/dom-semantics.d.ts +8 -0
  38. package/dist/shared/dom-semantics.js +69 -0
  39. package/dist/shared/instrumentation/instrument.js +101 -5
  40. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  41. package/dist/shared/llm/client.js +3 -1
  42. package/dist/shared/logger/index.js +4 -1
  43. package/dist/shared/run/api.js +3 -1
  44. package/dist/shared/run/browser.js +47 -3
  45. package/dist/shared/state/session-state.d.ts +2 -1
  46. package/dist/shared/state/session-state.js +5 -2
  47. package/dist/shared/visualization/ghost-cursor.js +36 -14
  48. package/dist/shared/visualization/highlight.js +9 -6
  49. package/dist/shared/workflow/workflow.d.ts +4 -5
  50. package/dist/shared/workflow/workflow.js +3 -5
  51. package/package.json +6 -2
  52. package/scripts/check-skills-sync.mjs +25 -0
  53. package/scripts/compare-eval-summary.mjs +47 -0
  54. package/scripts/postinstall.mjs +15 -15
  55. package/scripts/prepare-release.sh +97 -0
  56. package/scripts/skills-libretto.mjs +103 -0
  57. package/scripts/summarize-evals.mjs +135 -0
  58. package/scripts/sync-skills.mjs +12 -0
  59. package/skills/libretto/SKILL.md +132 -54
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +210 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/execution.ts +233 -102
  69. package/src/cli/commands/init.ts +37 -33
  70. package/src/cli/commands/logs.ts +7 -7
  71. package/src/cli/commands/shared.ts +36 -37
  72. package/src/cli/commands/snapshot.ts +44 -59
  73. package/src/cli/core/ai-config.ts +24 -4
  74. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  75. package/src/cli/core/browser.ts +260 -49
  76. package/src/cli/core/context.ts +7 -2
  77. package/src/cli/core/session-telemetry.ts +449 -197
  78. package/src/cli/core/session.ts +21 -7
  79. package/src/cli/core/snapshot-analyzer.ts +26 -46
  80. package/src/cli/core/snapshot-api-config.ts +170 -175
  81. package/src/cli/core/telemetry.ts +39 -4
  82. package/src/cli/framework/simple-cli.ts +144 -77
  83. package/src/cli/router.ts +13 -21
  84. package/src/cli/workers/run-integration-runtime.ts +36 -9
  85. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  86. package/src/cli/workers/run-integration-worker.ts +1 -4
  87. package/src/index.ts +73 -66
  88. package/src/runtime/download/download.ts +62 -58
  89. package/src/runtime/download/index.ts +5 -5
  90. package/src/runtime/extract/extract.ts +71 -61
  91. package/src/runtime/network/index.ts +3 -3
  92. package/src/runtime/network/network.ts +99 -93
  93. package/src/runtime/recovery/agent.ts +217 -212
  94. package/src/runtime/recovery/errors.ts +107 -104
  95. package/src/runtime/recovery/index.ts +3 -3
  96. package/src/runtime/recovery/recovery.ts +38 -35
  97. package/src/shared/condense-dom/condense-dom.ts +27 -82
  98. package/src/shared/config/config.ts +0 -19
  99. package/src/shared/config/index.ts +0 -5
  100. package/src/shared/debug/pause.ts +57 -51
  101. package/src/shared/dom-semantics.ts +68 -0
  102. package/src/shared/instrumentation/errors.ts +64 -62
  103. package/src/shared/instrumentation/index.ts +5 -5
  104. package/src/shared/instrumentation/instrument.ts +339 -209
  105. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  106. package/src/shared/llm/client.ts +181 -174
  107. package/src/shared/llm/types.ts +39 -39
  108. package/src/shared/logger/index.ts +11 -4
  109. package/src/shared/logger/logger.ts +312 -306
  110. package/src/shared/logger/sinks.ts +118 -114
  111. package/src/shared/paths/paths.ts +50 -49
  112. package/src/shared/paths/repo-root.ts +17 -17
  113. package/src/shared/run/api.ts +5 -1
  114. package/src/shared/run/browser.ts +65 -3
  115. package/src/shared/state/index.ts +9 -9
  116. package/src/shared/state/session-state.ts +46 -43
  117. package/src/shared/visualization/ghost-cursor.ts +180 -149
  118. package/src/shared/visualization/highlight.ts +89 -86
  119. package/src/shared/visualization/index.ts +13 -13
  120. package/src/shared/workflow/workflow.ts +19 -25
  121. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  122. package/skills/libretto/references/user-action-log.md +0 -31
@@ -1,4 +1,12 @@
1
1
  import type { BrowserContext, Page } from "playwright";
2
+ import {
3
+ filterSemanticClasses,
4
+ INTERACTIVE_ROLE_NAMES,
5
+ INTERACTIVE_TAG_NAMES,
6
+ isObfuscatedClass,
7
+ TEST_ATTRIBUTE_NAMES,
8
+ TRUSTED_ATTRIBUTE_NAMES,
9
+ } from "../../shared/dom-semantics.js";
2
10
 
3
11
  type TelemetryEntry = Record<string, unknown>;
4
12
 
@@ -13,7 +21,8 @@ type InstallSessionTelemetryOptions = {
13
21
  export async function installSessionTelemetry(
14
22
  options: InstallSessionTelemetryOptions,
15
23
  ): Promise<void> {
16
- const STATIC_EXT_RE = /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
24
+ const STATIC_EXT_RE =
25
+ /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
17
26
  const { context, initialPage, logAction, logNetwork } = options;
18
27
  const includeUserDomActions = options.includeUserDomActions ?? false;
19
28
  const pageIdCache = new WeakMap<Page, string>();
@@ -25,10 +34,12 @@ export async function installSessionTelemetry(
25
34
  const cdpSession = await context.newCDPSession(page);
26
35
  try {
27
36
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
28
- const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })?.targetInfo
29
- ?.targetId;
37
+ const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
38
+ ?.targetInfo?.targetId;
30
39
  if (typeof targetId !== "string" || targetId.length === 0) {
31
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
40
+ throw new Error(
41
+ `Could not resolve target id for page at URL "${page.url()}".`,
42
+ );
32
43
  }
33
44
  pageIdCache.set(page, targetId);
34
45
  return targetId;
@@ -51,7 +62,10 @@ export async function installSessionTelemetry(
51
62
  });
52
63
  };
53
64
 
54
- const markApiActionInProgress = async (page: Page, inProgress: boolean): Promise<void> => {
65
+ const markApiActionInProgress = async (
66
+ page: Page,
67
+ inProgress: boolean,
68
+ ): Promise<void> => {
55
69
  await page.evaluate((flag) => {
56
70
  (window as any).__btApiActionInProgress = flag;
57
71
  }, inProgress);
@@ -180,209 +194,445 @@ export async function installSessionTelemetry(
180
194
  return locator;
181
195
  };
182
196
 
183
- const installUserDomTracking = async (page: Page, pageId: string): Promise<void> => {
184
- if (exposedPages.has(page)) return;
185
- exposedPages.add(page);
197
+ const createUserDomTrackingInitScript = (): string => {
198
+ const selectorAttributeNames = [
199
+ "id",
200
+ ...TEST_ATTRIBUTE_NAMES,
201
+ "name",
202
+ "for",
203
+ "role",
204
+ "aria-label",
205
+ "title",
206
+ "placeholder",
207
+ "alt",
208
+ "href",
209
+ "type",
210
+ ];
211
+
212
+ return `
213
+ (() => {
214
+ if (window.__btDomListenersInstalled) return;
215
+ window.__btDomListenersInstalled = true;
216
+
217
+ const TEST_ATTRS = new Set(${JSON.stringify([...TEST_ATTRIBUTE_NAMES])});
218
+ const TRUSTED_ATTRS = new Set(${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])});
219
+ const INTERACTIVE_TAGS = new Set(${JSON.stringify([...INTERACTIVE_TAG_NAMES])});
220
+ const INTERACTIVE_ROLES = new Set(${JSON.stringify([...INTERACTIVE_ROLE_NAMES])});
221
+ const SELECTOR_ATTRS = ${JSON.stringify(selectorAttributeNames)};
222
+
223
+ ${filterSemanticClasses.toString()}
224
+ ${isObfuscatedClass.toString()}
225
+
226
+ const normalizeWhitespace = (value) =>
227
+ String(value || "").replace(/\\s+/g, " ").trim();
228
+
229
+ const clipText = (value, limit = 120) => {
230
+ const normalized = normalizeWhitespace(value);
231
+ if (!normalized) return "";
232
+ return normalized.length > limit
233
+ ? \`\${normalized.slice(0, limit - 1)}…\`
234
+ : normalized;
235
+ };
186
236
 
187
- await page.exposeFunction("__btActionLog", (jsonStr: string) => {
188
- const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
189
- emitAction({
190
- pageId,
191
- source: "user",
192
- ...parsed,
193
- });
237
+ const cssEscape = (value) => {
238
+ if (globalThis.CSS && typeof globalThis.CSS.escape === "function") {
239
+ return globalThis.CSS.escape(String(value));
240
+ }
241
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => {
242
+ const hex = char.charCodeAt(0).toString(16);
243
+ return \`\\\\\${hex} \`;
194
244
  });
245
+ };
195
246
 
196
- await page.addInitScript(() => {
197
- if ((window as any).__btDomListenersInstalled) return;
198
- (window as any).__btDomListenersInstalled = true;
199
-
200
- const identify = (el: any): string => {
201
- if (!el || !el.tagName) return "";
202
- const testId = el.getAttribute("data-testid");
203
- if (testId) return `[data-testid="${testId}"]`;
204
- const role = el.getAttribute("role") || "";
205
- const id = el.id;
206
- if (role && id) return `${role}#${id}`;
207
- const label =
208
- el.getAttribute("aria-label") ||
209
- (el.textContent || "").trim().slice(0, 30) ||
210
- "";
211
- if (role && label) return `${role} "${label}"`;
212
- const tag = el.tagName.toLowerCase();
213
- const cls =
214
- el.className && typeof el.className === "string"
215
- ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".")
216
- : "";
217
- return `${tag}${cls}`;
218
- };
247
+ const quoteAttrValue = (value) =>
248
+ String(value).replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
219
249
 
220
- let clickTimer: ReturnType<typeof setTimeout> | null = null;
221
- let pendingClick: { selector: string } | null = null;
222
-
223
- document.addEventListener(
224
- "click",
225
- (event) => {
226
- if ((window as any).__btApiActionInProgress) return;
227
- const target = event.target as any;
228
- const selector = identify(target);
229
- if (target?.type === "checkbox") {
230
- (window as any).__btActionLog(
231
- JSON.stringify({
232
- action: target.checked ? "check" : "uncheck",
233
- selector,
234
- success: true,
235
- }),
236
- );
237
- return;
238
- }
239
- pendingClick = { selector };
240
- if (clickTimer) clearTimeout(clickTimer);
241
- clickTimer = setTimeout(() => {
242
- if (pendingClick) {
243
- (window as any).__btActionLog(
244
- JSON.stringify({
245
- action: "click",
246
- selector: pendingClick.selector,
247
- success: true,
248
- }),
249
- );
250
- }
251
- pendingClick = null;
252
- clickTimer = null;
253
- }, 200);
254
- },
255
- true,
256
- );
250
+ const isElementNode = (value) => value instanceof Element;
257
251
 
258
- document.addEventListener(
259
- "dblclick",
260
- (event) => {
261
- if ((window as any).__btApiActionInProgress) return;
262
- if (clickTimer) {
263
- clearTimeout(clickTimer);
264
- clickTimer = null;
265
- pendingClick = null;
266
- }
267
- const selector = identify(event.target);
268
- (window as any).__btActionLog(
269
- JSON.stringify({ action: "dblclick", selector, success: true }),
270
- );
271
- },
272
- true,
252
+ const isInteractiveElement = (el) => {
253
+ if (!el || !el.tagName) return false;
254
+ const tagName = el.tagName.toLowerCase();
255
+ if (INTERACTIVE_TAGS.has(tagName)) return true;
256
+ if (el.hasAttribute("tabindex") || el.hasAttribute("contenteditable")) {
257
+ return true;
258
+ }
259
+ const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
260
+ return Boolean(role) && INTERACTIVE_ROLES.has(role);
261
+ };
262
+
263
+ const getFilteredClassSelector = (el) => {
264
+ const className =
265
+ typeof el.className === "string"
266
+ ? filterSemanticClasses(el.className)
267
+ : "";
268
+ const classes = className
269
+ .split(/\\s+/)
270
+ .filter(Boolean)
271
+ .slice(0, 2);
272
+ if (classes.length === 0) return "";
273
+ return classes.map((cls) => \`.\${cssEscape(cls)}\`).join("");
274
+ };
275
+
276
+ const getMeaningfulAttrValue = (el, attrName) => {
277
+ if (!el.hasAttribute(attrName)) return "";
278
+ const value = clipText(el.getAttribute(attrName) || "", 80);
279
+ if (!value) return "";
280
+ if (attrName === "href" && value.startsWith("javascript:")) return "";
281
+ return value;
282
+ };
283
+
284
+ const buildSelectorForElement = (el) => {
285
+ if (!isElementNode(el)) return "";
286
+ const tagName = el.tagName.toLowerCase();
287
+ const id = clipText(el.id || "", 80);
288
+ if (id) {
289
+ return \`\${tagName}#\${cssEscape(id)}\`;
290
+ }
291
+
292
+ for (const attrName of SELECTOR_ATTRS) {
293
+ if (attrName === "id") continue;
294
+ const value = getMeaningfulAttrValue(el, attrName);
295
+ if (!value) continue;
296
+ if (attrName.startsWith("data-")) {
297
+ return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
298
+ }
299
+ return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
300
+ }
301
+
302
+ const classSelector = getFilteredClassSelector(el);
303
+ if (classSelector) {
304
+ return \`\${tagName}\${classSelector}\`;
305
+ }
306
+
307
+ return tagName;
308
+ };
309
+
310
+ const getElementSummary = (el) => {
311
+ if (!isElementNode(el)) return "";
312
+ const selector = buildSelectorForElement(el);
313
+ const role = clipText(el.getAttribute("role") || "", 40);
314
+ const text = getElementText(el);
315
+ const suffix = [];
316
+ if (role) suffix.push(\`role=\${role}\`);
317
+ if (text) suffix.push(\`text="\${text}"\`);
318
+ return suffix.length > 0 ? \`\${selector} [\${suffix.join(", ")}]\` : selector;
319
+ };
320
+
321
+ const getElementText = (el) => {
322
+ if (!isElementNode(el)) return "";
323
+ const tagName = el.tagName.toLowerCase();
324
+ if (tagName === "input") {
325
+ const inputType = normalizeWhitespace(el.getAttribute("type")).toLowerCase();
326
+ if (inputType === "password") return "";
327
+ const value = clipText(el.value || el.getAttribute("value") || "", 80);
328
+ if (value) return value;
329
+ }
330
+
331
+ const attrCandidates = [
332
+ el.getAttribute("aria-label"),
333
+ el.getAttribute("title"),
334
+ el.getAttribute("placeholder"),
335
+ el.getAttribute("alt"),
336
+ el.textContent,
337
+ ];
338
+
339
+ for (const candidate of attrCandidates) {
340
+ const text = clipText(candidate || "", 120);
341
+ if (text.length >= 2) return text;
342
+ }
343
+
344
+ return "";
345
+ };
346
+
347
+ const isMeaningfulAncestor = (el) => {
348
+ if (!isElementNode(el)) return false;
349
+ const tagName = el.tagName.toLowerCase();
350
+ if (isInteractiveElement(el)) return true;
351
+ if (
352
+ [
353
+ "tr",
354
+ "li",
355
+ "td",
356
+ "th",
357
+ "label",
358
+ "section",
359
+ "article",
360
+ "dialog",
361
+ "fieldset",
362
+ "summary",
363
+ ].includes(tagName)
364
+ ) {
365
+ return true;
366
+ }
367
+ if (el.id) return true;
368
+ for (const attrName of TEST_ATTRS) {
369
+ if (el.hasAttribute(attrName)) return true;
370
+ }
371
+ const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
372
+ return Boolean(role);
373
+ };
374
+
375
+ const hasStrongSelectorSignal = (el, selector) => {
376
+ if (!isElementNode(el)) return false;
377
+ if (!selector) return false;
378
+ const tagName = el.tagName.toLowerCase();
379
+ if (el.id) return true;
380
+ if (isInteractiveElement(el)) return true;
381
+ if (getFilteredClassSelector(el)) return true;
382
+ for (const attrName of SELECTOR_ATTRS) {
383
+ if (attrName === "id") continue;
384
+ if (getMeaningfulAttrValue(el, attrName)) return true;
385
+ }
386
+ return selector !== tagName;
387
+ };
388
+
389
+ const getAncestorSelectors = (target) => {
390
+ const selectors = [];
391
+ let current = isElementNode(target) ? target : null;
392
+ let depth = 0;
393
+ while (current && depth < 7) {
394
+ const selector = buildSelectorForElement(current);
395
+ if (
396
+ selector &&
397
+ !selectors.includes(selector) &&
398
+ (
399
+ (depth === 0 && hasStrongSelectorSignal(current, selector)) ||
400
+ isMeaningfulAncestor(current)
401
+ )
402
+ ) {
403
+ selectors.push(selector);
404
+ }
405
+ current = current.parentElement;
406
+ depth += 1;
407
+ }
408
+ if (selectors.length === 0 && isElementNode(target)) {
409
+ selectors.push(buildSelectorForElement(target));
410
+ }
411
+ return selectors;
412
+ };
413
+
414
+ const getNearbyText = (target) => {
415
+ let current = isElementNode(target) ? target : null;
416
+ let depth = 0;
417
+ while (current && depth < 6) {
418
+ const text = getElementText(current);
419
+ if (text.length >= 2) return text;
420
+ current = current.parentElement;
421
+ depth += 1;
422
+ }
423
+ return "";
424
+ };
425
+
426
+ const getComposedPathSummary = (event) => {
427
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
428
+ return path
429
+ .filter((entry) => isElementNode(entry))
430
+ .slice(0, 6)
431
+ .map((entry) => getElementSummary(entry));
432
+ };
433
+
434
+ const getCoordinates = (event) => {
435
+ if (!(event instanceof MouseEvent)) return undefined;
436
+ return {
437
+ x: Math.round(event.clientX),
438
+ y: Math.round(event.clientY),
439
+ };
440
+ };
441
+
442
+ const buildActionPayload = (event, action, extra = {}) => {
443
+ const target = isElementNode(event.target) ? event.target : null;
444
+ const ancestorSelectors = getAncestorSelectors(target);
445
+ const bestSemanticSelector = ancestorSelectors[0] || "";
446
+ const targetSelector = target ? buildSelectorForElement(target) : "";
447
+ return {
448
+ action,
449
+ bestSemanticSelector: bestSemanticSelector || undefined,
450
+ targetSelector: targetSelector || undefined,
451
+ ancestorSelectors: ancestorSelectors.length > 0 ? ancestorSelectors : undefined,
452
+ nearbyText: getNearbyText(target) || undefined,
453
+ composedPath: getComposedPathSummary(event),
454
+ coordinates: getCoordinates(event),
455
+ success: true,
456
+ ...extra,
457
+ };
458
+ };
459
+
460
+ let clickTimer = null;
461
+ let pendingClick = null;
462
+
463
+ document.addEventListener(
464
+ "click",
465
+ (event) => {
466
+ if (window.__btApiActionInProgress) return;
467
+ const target = isElementNode(event.target) ? event.target : null;
468
+ if (target?.type === "checkbox") {
469
+ window.__btActionLog(
470
+ JSON.stringify(
471
+ buildActionPayload(event, target.checked ? "check" : "uncheck"),
472
+ ),
473
+ );
474
+ return;
475
+ }
476
+
477
+ pendingClick = buildActionPayload(event, "click");
478
+ if (clickTimer) clearTimeout(clickTimer);
479
+ clickTimer = setTimeout(() => {
480
+ if (pendingClick) {
481
+ window.__btActionLog(JSON.stringify(pendingClick));
482
+ }
483
+ pendingClick = null;
484
+ clickTimer = null;
485
+ }, 200);
486
+ },
487
+ true,
488
+ );
489
+
490
+ document.addEventListener(
491
+ "dblclick",
492
+ (event) => {
493
+ if (window.__btApiActionInProgress) return;
494
+ if (clickTimer) {
495
+ clearTimeout(clickTimer);
496
+ clickTimer = null;
497
+ pendingClick = null;
498
+ }
499
+ window.__btActionLog(
500
+ JSON.stringify(buildActionPayload(event, "dblclick")),
273
501
  );
502
+ },
503
+ true,
504
+ );
505
+
506
+ const inputTimers = new WeakMap();
507
+ document.addEventListener(
508
+ "input",
509
+ (event) => {
510
+ if (window.__btApiActionInProgress) return;
511
+ const target = isElementNode(event.target) ? event.target : null;
512
+ if (!target) return;
513
+
514
+ if (target.tagName === "SELECT") {
515
+ window.__btActionLog(
516
+ JSON.stringify(
517
+ buildActionPayload(event, "selectOption", {
518
+ value: target.value,
519
+ }),
520
+ ),
521
+ );
522
+ return;
523
+ }
274
524
 
275
- const inputTimers = new WeakMap<any, ReturnType<typeof setTimeout>>();
276
- document.addEventListener(
277
- "input",
278
- (event) => {
279
- if ((window as any).__btApiActionInProgress) return;
280
- const target = event.target as any;
281
- const selector = identify(target);
282
- if (target.tagName === "SELECT") {
283
- (window as any).__btActionLog(
284
- JSON.stringify({
285
- action: "selectOption",
286
- selector,
287
- value: target.value,
288
- success: true,
525
+ const existing = inputTimers.get(target);
526
+ if (existing) clearTimeout(existing);
527
+ inputTimers.set(
528
+ target,
529
+ setTimeout(() => {
530
+ inputTimers.delete(target);
531
+ window.__btActionLog(
532
+ JSON.stringify(
533
+ buildActionPayload(event, "fill", {
534
+ value: String(target.value || "").slice(0, 100),
289
535
  }),
290
- );
291
- return;
292
- }
293
- const existing = inputTimers.get(target);
294
- if (existing) clearTimeout(existing);
295
- inputTimers.set(
296
- target,
297
- setTimeout(() => {
298
- inputTimers.delete(target);
299
- (window as any).__btActionLog(
300
- JSON.stringify({
301
- action: "fill",
302
- selector,
303
- value: (target.value || "").slice(0, 100),
304
- success: true,
305
- }),
306
- );
307
- }, 500),
536
+ ),
308
537
  );
309
- },
310
- true,
538
+ }, 500),
311
539
  );
312
-
313
- const specialKeys = new Set([
314
- "Enter",
315
- "Escape",
316
- "Tab",
317
- "Backspace",
318
- "Delete",
319
- "ArrowUp",
320
- "ArrowDown",
321
- "ArrowLeft",
322
- "ArrowRight",
323
- "Home",
324
- "End",
325
- "PageUp",
326
- "PageDown",
327
- "F1",
328
- "F2",
329
- "F3",
330
- "F4",
331
- "F5",
332
- "F6",
333
- "F7",
334
- "F8",
335
- "F9",
336
- "F10",
337
- "F11",
338
- "F12",
339
- ]);
340
- document.addEventListener(
341
- "keydown",
342
- (event) => {
343
- if ((window as any).__btApiActionInProgress) return;
344
- const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
345
- if (!isShortcut && !specialKeys.has(event.key)) return;
346
- const selector = identify(event.target);
347
- const keyDesc =
348
- (event.ctrlKey ? "Ctrl+" : "") +
349
- (event.metaKey ? "Meta+" : "") +
350
- (event.altKey ? "Alt+" : "") +
351
- (event.shiftKey ? "Shift+" : "") +
352
- event.key;
353
- (window as any).__btActionLog(
354
- JSON.stringify({
355
- action: "press",
356
- selector,
357
- value: keyDesc,
358
- success: true,
359
- }),
360
- );
361
- },
362
- true,
540
+ },
541
+ true,
542
+ );
543
+
544
+ const specialKeys = new Set([
545
+ "Enter",
546
+ "Escape",
547
+ "Tab",
548
+ "Backspace",
549
+ "Delete",
550
+ "ArrowUp",
551
+ "ArrowDown",
552
+ "ArrowLeft",
553
+ "ArrowRight",
554
+ "Home",
555
+ "End",
556
+ "PageUp",
557
+ "PageDown",
558
+ "F1",
559
+ "F2",
560
+ "F3",
561
+ "F4",
562
+ "F5",
563
+ "F6",
564
+ "F7",
565
+ "F8",
566
+ "F9",
567
+ "F10",
568
+ "F11",
569
+ "F12",
570
+ ]);
571
+
572
+ document.addEventListener(
573
+ "keydown",
574
+ (event) => {
575
+ if (window.__btApiActionInProgress) return;
576
+ const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
577
+ if (!isShortcut && !specialKeys.has(event.key)) return;
578
+ const keyDesc =
579
+ (event.ctrlKey ? "Ctrl+" : "") +
580
+ (event.metaKey ? "Meta+" : "") +
581
+ (event.altKey ? "Alt+" : "") +
582
+ (event.shiftKey ? "Shift+" : "") +
583
+ event.key;
584
+ window.__btActionLog(
585
+ JSON.stringify(
586
+ buildActionPayload(event, "press", {
587
+ value: keyDesc,
588
+ }),
589
+ ),
363
590
  );
591
+ },
592
+ true,
593
+ );
594
+
595
+ let scrollTimer = null;
596
+ document.addEventListener(
597
+ "scroll",
598
+ () => {
599
+ if (window.__btApiActionInProgress) return;
600
+ if (scrollTimer) clearTimeout(scrollTimer);
601
+ scrollTimer = setTimeout(() => {
602
+ scrollTimer = null;
603
+ window.__btActionLog(
604
+ JSON.stringify({
605
+ action: "scroll",
606
+ bestSemanticSelector: "document",
607
+ success: true,
608
+ value: \`y=\${window.scrollY}\`,
609
+ }),
610
+ );
611
+ }, 300);
612
+ },
613
+ true,
614
+ );
615
+ })();
616
+ `;
617
+ };
364
618
 
365
- let scrollTimer: ReturnType<typeof setTimeout> | null = null;
366
- document.addEventListener(
367
- "scroll",
368
- () => {
369
- if ((window as any).__btApiActionInProgress) return;
370
- if (scrollTimer) clearTimeout(scrollTimer);
371
- scrollTimer = setTimeout(() => {
372
- scrollTimer = null;
373
- (window as any).__btActionLog(
374
- JSON.stringify({
375
- action: "scroll",
376
- selector: "document",
377
- value: `y=${window.scrollY}`,
378
- success: true,
379
- }),
380
- );
381
- }, 300);
382
- },
383
- true,
384
- );
619
+ const installUserDomTracking = async (
620
+ page: Page,
621
+ pageId: string,
622
+ ): Promise<void> => {
623
+ if (exposedPages.has(page)) return;
624
+ exposedPages.add(page);
625
+
626
+ await page.exposeFunction("__btActionLog", (jsonStr: string) => {
627
+ const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
628
+ emitAction({
629
+ pageId,
630
+ source: "user",
631
+ ...parsed,
632
+ });
385
633
  });
634
+
635
+ await page.addInitScript({ content: createUserDomTrackingInitScript() });
386
636
  };
387
637
 
388
638
  const wrapPageActions = (page: Page, pageId: string): void => {
@@ -425,7 +675,8 @@ export async function installSessionTelemetry(
425
675
  action: method,
426
676
  source: "agent",
427
677
  selector: typeof args[0] === "string" ? args[0] : undefined,
428
- value: args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
678
+ value:
679
+ args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
429
680
  duration: Date.now() - start,
430
681
  success: true,
431
682
  });
@@ -497,7 +748,8 @@ export async function installSessionTelemetry(
497
748
  page.on("response", async (response) => {
498
749
  const request = response.request();
499
750
  const url = request.url();
500
- if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://")) return;
751
+ if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
752
+ return;
501
753
  emitNetwork({
502
754
  pageId,
503
755
  method: request.method(),