libretto 0.5.3-experimental.5 → 0.5.3

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 (126) hide show
  1. package/README.md +114 -37
  2. package/README.template.md +160 -0
  3. package/dist/cli/cli.js +22 -97
  4. package/dist/cli/commands/browser.js +86 -59
  5. package/dist/cli/commands/deploy.js +148 -0
  6. package/dist/cli/commands/execution.js +218 -96
  7. package/dist/cli/commands/init.js +34 -29
  8. package/dist/cli/commands/logs.js +4 -5
  9. package/dist/cli/commands/shared.js +30 -29
  10. package/dist/cli/commands/snapshot.js +26 -39
  11. package/dist/cli/core/ai-config.js +21 -4
  12. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  13. package/dist/cli/core/browser.js +207 -37
  14. package/dist/cli/core/context.js +4 -1
  15. package/dist/cli/core/deploy-artifact.js +687 -0
  16. package/dist/cli/core/session-telemetry.js +434 -174
  17. package/dist/cli/core/session.js +21 -8
  18. package/dist/cli/core/snapshot-analyzer.js +14 -31
  19. package/dist/cli/core/snapshot-api-config.js +2 -6
  20. package/dist/cli/core/telemetry.js +20 -4
  21. package/dist/cli/framework/simple-cli.js +144 -43
  22. package/dist/cli/router.js +16 -21
  23. package/dist/cli/workers/run-integration-runtime.js +25 -45
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/cli/workers/run-integration-worker.js +1 -4
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +13 -10
  28. package/dist/runtime/download/download.js +5 -1
  29. package/dist/runtime/extract/extract.js +11 -2
  30. package/dist/runtime/network/network.js +8 -1
  31. package/dist/runtime/recovery/agent.js +6 -2
  32. package/dist/runtime/recovery/errors.js +3 -1
  33. package/dist/runtime/recovery/recovery.js +3 -1
  34. package/dist/shared/condense-dom/condense-dom.js +17 -69
  35. package/dist/shared/config/config.d.ts +1 -9
  36. package/dist/shared/config/config.js +0 -18
  37. package/dist/shared/config/index.d.ts +2 -1
  38. package/dist/shared/config/index.js +0 -10
  39. package/dist/shared/debug/pause.js +9 -3
  40. package/dist/shared/dom-semantics.d.ts +8 -0
  41. package/dist/shared/dom-semantics.js +69 -0
  42. package/dist/shared/instrumentation/instrument.js +101 -5
  43. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  44. package/dist/shared/llm/client.js +3 -1
  45. package/dist/shared/logger/index.js +4 -1
  46. package/dist/shared/run/api.js +3 -1
  47. package/dist/shared/run/browser.js +47 -3
  48. package/dist/shared/state/session-state.d.ts +2 -1
  49. package/dist/shared/state/session-state.js +5 -2
  50. package/dist/shared/visualization/ghost-cursor.js +36 -14
  51. package/dist/shared/visualization/highlight.js +9 -6
  52. package/dist/shared/workflow/workflow.d.ts +18 -10
  53. package/dist/shared/workflow/workflow.js +50 -5
  54. package/package.json +14 -6
  55. package/scripts/generate-changelog.ts +132 -0
  56. package/scripts/postinstall.mjs +4 -3
  57. package/scripts/skills-libretto.mjs +2 -88
  58. package/scripts/summarize-evals.mjs +32 -10
  59. package/skills/libretto/SKILL.md +132 -62
  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 +176 -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/deploy.ts +198 -0
  69. package/src/cli/commands/execution.ts +251 -111
  70. package/src/cli/commands/init.ts +37 -33
  71. package/src/cli/commands/logs.ts +7 -7
  72. package/src/cli/commands/shared.ts +36 -37
  73. package/src/cli/commands/snapshot.ts +44 -59
  74. package/src/cli/core/ai-config.ts +24 -4
  75. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  76. package/src/cli/core/browser.ts +260 -49
  77. package/src/cli/core/context.ts +7 -2
  78. package/src/cli/core/deploy-artifact.ts +938 -0
  79. package/src/cli/core/session-telemetry.ts +449 -197
  80. package/src/cli/core/session.ts +21 -7
  81. package/src/cli/core/snapshot-analyzer.ts +26 -46
  82. package/src/cli/core/snapshot-api-config.ts +170 -175
  83. package/src/cli/core/telemetry.ts +39 -4
  84. package/src/cli/framework/simple-cli.ts +281 -98
  85. package/src/cli/router.ts +15 -21
  86. package/src/cli/workers/run-integration-runtime.ts +35 -57
  87. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  88. package/src/cli/workers/run-integration-worker.ts +1 -4
  89. package/src/index.ts +77 -67
  90. package/src/runtime/download/download.ts +62 -58
  91. package/src/runtime/download/index.ts +5 -5
  92. package/src/runtime/extract/extract.ts +71 -61
  93. package/src/runtime/network/index.ts +3 -3
  94. package/src/runtime/network/network.ts +99 -93
  95. package/src/runtime/recovery/agent.ts +217 -212
  96. package/src/runtime/recovery/errors.ts +107 -104
  97. package/src/runtime/recovery/index.ts +3 -3
  98. package/src/runtime/recovery/recovery.ts +38 -35
  99. package/src/shared/condense-dom/condense-dom.ts +27 -82
  100. package/src/shared/config/config.ts +0 -19
  101. package/src/shared/config/index.ts +0 -5
  102. package/src/shared/debug/pause.ts +57 -51
  103. package/src/shared/dom-semantics.ts +68 -0
  104. package/src/shared/instrumentation/errors.ts +64 -62
  105. package/src/shared/instrumentation/index.ts +5 -5
  106. package/src/shared/instrumentation/instrument.ts +339 -209
  107. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  108. package/src/shared/llm/client.ts +181 -174
  109. package/src/shared/llm/types.ts +39 -39
  110. package/src/shared/logger/index.ts +11 -4
  111. package/src/shared/logger/logger.ts +312 -306
  112. package/src/shared/logger/sinks.ts +118 -114
  113. package/src/shared/paths/paths.ts +50 -49
  114. package/src/shared/paths/repo-root.ts +17 -17
  115. package/src/shared/run/api.ts +5 -1
  116. package/src/shared/run/browser.ts +65 -3
  117. package/src/shared/state/index.ts +9 -9
  118. package/src/shared/state/session-state.ts +46 -43
  119. package/src/shared/visualization/ghost-cursor.ts +180 -149
  120. package/src/shared/visualization/highlight.ts +89 -86
  121. package/src/shared/visualization/index.ts +13 -13
  122. package/src/shared/workflow/workflow.ts +107 -30
  123. package/scripts/check-skills-sync.mjs +0 -23
  124. package/scripts/prepare-release.sh +0 -97
  125. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
  126. package/skills/libretto/references/user-action-log.md +0 -31
package/dist/index.d.ts CHANGED
@@ -10,12 +10,11 @@ export { ExtractOptions, extractFromPage } from './runtime/extract/extract.js';
10
10
  export { PageRequestOptions, RequestConfig, pageRequest } from './runtime/network/network.js';
11
11
  export { DownloadResult, DownloadViaClickOptions, SaveDownloadOptions, downloadAndSave, downloadViaClick } from './runtime/download/download.js';
12
12
  export { pause } from './shared/debug/pause.js';
13
- export { isDebugMode, isDryRun, shouldPauseBeforeMutation } from './shared/config/config.js';
14
13
  export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instrumentContext, instrumentPage } from './shared/instrumentation/instrument.js';
15
14
  export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
16
15
  export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
17
16
  export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
18
- export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, LibrettoWorkflowMetadata, workflow } from './shared/workflow/workflow.js';
17
+ export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow } from './shared/workflow/workflow.js';
19
18
  import 'zod';
20
19
  import 'ai';
21
20
  import 'playwright';
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
- import { Logger, defaultLogger } from "./shared/logger/logger.js";
3
+ import {
4
+ Logger,
5
+ defaultLogger
6
+ } from "./shared/logger/logger.js";
4
7
  import {
5
8
  createFileLogSink,
6
9
  prettyConsoleSink,
@@ -20,7 +23,9 @@ import { attemptWithRecovery } from "./runtime/recovery/recovery.js";
20
23
  import {
21
24
  detectSubmissionError
22
25
  } from "./runtime/recovery/errors.js";
23
- import { extractFromPage } from "./runtime/extract/extract.js";
26
+ import {
27
+ extractFromPage
28
+ } from "./runtime/extract/extract.js";
24
29
  import {
25
30
  pageRequest
26
31
  } from "./runtime/network/network.js";
@@ -29,11 +34,6 @@ import {
29
34
  downloadAndSave
30
35
  } from "./runtime/download/download.js";
31
36
  import { pause } from "./shared/debug/pause.js";
32
- import {
33
- isDebugMode,
34
- isDryRun,
35
- shouldPauseBeforeMutation
36
- } from "./shared/config/config.js";
37
37
  import {
38
38
  instrumentPage,
39
39
  installInstrumentation,
@@ -54,6 +54,9 @@ import {
54
54
  launchBrowser
55
55
  } from "./shared/run/api.js";
56
56
  import {
57
+ getWorkflowFromModuleExports,
58
+ getWorkflowsFromModuleExports,
59
+ isLibrettoWorkflow,
57
60
  LibrettoWorkflow,
58
61
  LIBRETTO_WORKFLOW_BRAND,
59
62
  workflow
@@ -92,13 +95,14 @@ export {
92
95
  ensureHighlightLayer,
93
96
  executeRecoveryAgent,
94
97
  extractFromPage,
98
+ getWorkflowFromModuleExports,
99
+ getWorkflowsFromModuleExports,
95
100
  ghostClick,
96
101
  hideGhostCursor,
97
102
  installInstrumentation,
98
103
  instrumentContext,
99
104
  instrumentPage,
100
- isDebugMode,
101
- isDryRun,
105
+ isLibrettoWorkflow,
102
106
  jsonlConsoleSink,
103
107
  launchBrowser,
104
108
  moveGhostCursor,
@@ -108,7 +112,6 @@ export {
108
112
  pause,
109
113
  prettyConsoleSink,
110
114
  serializeSessionState,
111
- shouldPauseBeforeMutation,
112
115
  showHighlight,
113
116
  workflow
114
117
  };
@@ -29,7 +29,11 @@ async function downloadViaClick(page, selector, options) {
29
29
  }
30
30
  async function downloadAndSave(page, selector, options) {
31
31
  const { savePath, ...downloadOpts } = options ?? {};
32
- const { buffer, filename } = await downloadViaClick(page, selector, downloadOpts);
32
+ const { buffer, filename } = await downloadViaClick(
33
+ page,
34
+ selector,
35
+ downloadOpts
36
+ );
33
37
  const dest = resolve(savePath ?? filename);
34
38
  await writeFile(dest, buffer);
35
39
  options?.logger?.info("download:saved", {
@@ -1,6 +1,15 @@
1
- import { defaultLogger } from "../../shared/logger/logger.js";
1
+ import {
2
+ defaultLogger
3
+ } from "../../shared/logger/logger.js";
2
4
  async function extractFromPage(options) {
3
- const { page, instruction, schema, selector, logger = defaultLogger, llmClient } = options;
5
+ const {
6
+ page,
7
+ instruction,
8
+ schema,
9
+ selector,
10
+ logger = defaultLogger,
11
+ llmClient
12
+ } = options;
4
13
  let screenshot;
5
14
  let domContent;
6
15
  if (selector) {
@@ -1,5 +1,12 @@
1
1
  async function pageRequest(page, config, options) {
2
- const { url, method = "GET", headers = {}, body, bodyType = "json", responseType = "json" } = config;
2
+ const {
3
+ url,
4
+ method = "GET",
5
+ headers = {},
6
+ body,
7
+ bodyType = "json",
8
+ responseType = "json"
9
+ } = config;
3
10
  const { logger, schema } = options ?? {};
4
11
  const startTime = Date.now();
5
12
  const fetchHeaders = { ...headers };
@@ -1,4 +1,6 @@
1
- import { defaultLogger } from "../../shared/logger/logger.js";
1
+ import {
2
+ defaultLogger
3
+ } from "../../shared/logger/logger.js";
2
4
  function delay(ms) {
3
5
  return new Promise((resolve) => setTimeout(resolve, ms));
4
6
  }
@@ -86,7 +88,9 @@ async function executeBrowserAction(page, action, logger = defaultLogger) {
86
88
  if (point) await page.mouse.move(point.x, point.y);
87
89
  }
88
90
  await page.mouse.up();
89
- logger.info(`Dragged from (${start.x}, ${start.y}) to (${end.x}, ${end.y})`);
91
+ logger.info(
92
+ `Dragged from (${start.x}, ${start.y}) to (${end.x}, ${end.y})`
93
+ );
90
94
  }
91
95
  break;
92
96
  }
@@ -1,4 +1,6 @@
1
- import { defaultLogger } from "../../shared/logger/logger.js";
1
+ import {
2
+ defaultLogger
3
+ } from "../../shared/logger/logger.js";
2
4
  import { z } from "zod";
3
5
  const detectSubmissionErrorSchema = z.object({
4
6
  hasError: z.boolean().describe("Whether an error is visible on the page"),
@@ -1,4 +1,6 @@
1
- import { defaultLogger } from "../../shared/logger/logger.js";
1
+ import {
2
+ defaultLogger
3
+ } from "../../shared/logger/logger.js";
2
4
  import { executeRecoveryAgent } from "./agent.js";
3
5
  async function attemptWithRecovery(page, fn, logger, llmClient) {
4
6
  const log = logger ?? defaultLogger;
@@ -1,22 +1,12 @@
1
- const TEST_ATTRS = /* @__PURE__ */ new Set(["data-testid", "data-test", "data-qa", "data-cy"]);
2
- const TRUSTED_ATTRS = /* @__PURE__ */ new Set([
3
- "id",
4
- "name",
5
- "for",
6
- "tabindex",
7
- "contenteditable",
8
- "role",
9
- "title",
10
- "alt",
11
- "type",
12
- "value",
13
- "placeholder",
14
- "autocomplete",
15
- "href",
16
- "action",
17
- "method",
18
- "src"
19
- ]);
1
+ import {
2
+ filterSemanticClasses,
3
+ INTERACTIVE_ROLE_NAMES,
4
+ INTERACTIVE_TAG_NAMES,
5
+ TEST_ATTRIBUTE_NAMES,
6
+ TRUSTED_ATTRIBUTE_NAMES
7
+ } from "../dom-semantics.js";
8
+ const TEST_ATTRS = new Set(TEST_ATTRIBUTE_NAMES);
9
+ const TRUSTED_ATTRS = new Set(TRUSTED_ATTRIBUTE_NAMES);
20
10
  const STATE_ATTRS = /* @__PURE__ */ new Set([
21
11
  "disabled",
22
12
  "hidden",
@@ -28,12 +18,7 @@ const STATE_ATTRS = /* @__PURE__ */ new Set([
28
18
  "open",
29
19
  "multiple"
30
20
  ]);
31
- const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
32
- ...STATE_ATTRS,
33
- "async",
34
- "defer",
35
- "nomodule"
36
- ]);
21
+ const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([...STATE_ATTRS, "async", "defer", "nomodule"]);
37
22
  const EMPTY_VALUE_DROP_ATTRS = /* @__PURE__ */ new Set([
38
23
  "alt",
39
24
  "autocomplete",
@@ -60,28 +45,8 @@ const SCRIPT_ATTRS = /* @__PURE__ */ new Set([
60
45
  "referrerpolicy"
61
46
  ]);
62
47
  const STYLE_TAG_ATTRS = /* @__PURE__ */ new Set(["media", "type", "nonce", "title"]);
63
- const INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
64
- "a",
65
- "button",
66
- "input",
67
- "select",
68
- "textarea",
69
- "form",
70
- "details",
71
- "dialog",
72
- "label"
73
- ]);
74
- const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
75
- "button",
76
- "link",
77
- "tab",
78
- "menuitem",
79
- "checkbox",
80
- "radio",
81
- "switch",
82
- "slider",
83
- "combobox"
84
- ]);
48
+ const INTERACTIVE_TAGS = new Set(INTERACTIVE_TAG_NAMES);
49
+ const INTERACTIVE_ROLES = new Set(INTERACTIVE_ROLE_NAMES);
85
50
  const OPEN_TAG_PATTERN = /<([a-zA-Z][\w:-]*)(\s(?:[^"'<>/]|"[^"]*"|'[^']*')*)?\s*(\/?)>/g;
86
51
  function condenseDom(html) {
87
52
  const originalLength = html.length;
@@ -170,12 +135,8 @@ function condenseDom(html) {
170
135
  }
171
136
  const hasAriaLabel = /aria-label\s*=/i.test(attrs);
172
137
  if (!hasAriaLabel) {
173
- const titleMatch = inner.match(
174
- /<title[^>]*>([^<]+)<\/title>/i
175
- );
176
- const descMatch = inner.match(
177
- /<desc[^>]*>([^<]+)<\/desc>/i
178
- );
138
+ const titleMatch = inner.match(/<title[^>]*>([^<]+)<\/title>/i);
139
+ const descMatch = inner.match(/<desc[^>]*>([^<]+)<\/desc>/i);
179
140
  const labelText = titleMatch?.[1]?.trim() || descMatch?.[1]?.trim();
180
141
  if (labelText) {
181
142
  keepAttrs.push(
@@ -346,21 +307,6 @@ function normalizeUrlValue(value) {
346
307
  return `${value.slice(0, 96)}[omitted]`;
347
308
  }
348
309
  }
349
- function filterSemanticClasses(value) {
350
- const classes = value.split(/\s+/).filter(Boolean);
351
- const kept = classes.filter((cls) => !isObfuscatedClass(cls));
352
- return kept.join(" ");
353
- }
354
- function isObfuscatedClass(cls) {
355
- if (cls.length > 80) return true;
356
- if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
357
- if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
358
- if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
359
- const digits = (cls.match(/[0-9]/g) || []).length;
360
- const letters = (cls.match(/[a-zA-Z]/g) || []).length;
361
- if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
362
- return false;
363
- }
364
310
  function parseAttributes(rawAttrs) {
365
311
  const attrs = [];
366
312
  const attrPattern = /([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
@@ -404,7 +350,9 @@ function shouldKeepCustomDataAttribute(tagName, attrName, value, interactive) {
404
350
  function looksMeaningfulToken(value) {
405
351
  if (!/^[a-z][a-z0-9-]{1,40}$/i.test(value)) return false;
406
352
  if (!/[a-z]{3}/i.test(value)) return false;
407
- if (/(track|metric|telemetry|analytics|component|display|loaded|token|dps|color|screen|strict|rehydr|fetch)/i.test(value)) {
353
+ if (/(track|metric|telemetry|analytics|component|display|loaded|token|dps|color|screen|strict|rehydr|fetch)/i.test(
354
+ value
355
+ )) {
408
356
  return false;
409
357
  }
410
358
  return true;
@@ -1,10 +1,2 @@
1
- /**
2
- * Runtime configuration for libretto.
3
- *
4
- * Values are derived from environment variables only.
5
- */
6
- declare function isDebugMode(): boolean;
7
- declare function isDryRun(): boolean;
8
- declare function shouldPauseBeforeMutation(): boolean;
9
1
 
10
- export { isDebugMode, isDryRun, shouldPauseBeforeMutation };
2
+ export { }
@@ -1,18 +0,0 @@
1
- function isDebugMode() {
2
- return process.env.LIBRETTO_DEBUG === "true";
3
- }
4
- function isDryRun() {
5
- const explicit = process.env.LIBRETTO_DRY_RUN;
6
- if (explicit !== void 0) {
7
- return explicit === "true";
8
- }
9
- return process.env.NODE_ENV === "development";
10
- }
11
- function shouldPauseBeforeMutation() {
12
- return isDryRun() && isDebugMode();
13
- }
14
- export {
15
- isDebugMode,
16
- isDryRun,
17
- shouldPauseBeforeMutation
18
- };
@@ -1 +1,2 @@
1
- export { isDebugMode, isDryRun, shouldPauseBeforeMutation } from './config.js';
1
+
2
+ export { }
@@ -1,10 +0,0 @@
1
- import {
2
- isDebugMode,
3
- isDryRun,
4
- shouldPauseBeforeMutation
5
- } from "./config.js";
6
- export {
7
- isDebugMode,
8
- isDryRun,
9
- shouldPauseBeforeMutation
10
- };
@@ -1,8 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { getSessionDir } from "../../cli/core/context.js";
4
- import { getPauseSignalPaths, removeSignalIfExists } from "../../cli/core/pause-signals.js";
5
- import { listSessionsWithStateFile, readSessionState } from "../../cli/core/session.js";
4
+ import {
5
+ getPauseSignalPaths,
6
+ removeSignalIfExists
7
+ } from "../../cli/core/pause-signals.js";
8
+ import {
9
+ listSessionsWithStateFile,
10
+ readSessionState
11
+ } from "../../cli/core/session.js";
6
12
  function isPidRunning(pid) {
7
13
  try {
8
14
  process.kill(pid, 0);
@@ -14,7 +20,7 @@ function isPidRunning(pid) {
14
20
  function getRunningSessions() {
15
21
  return listSessionsWithStateFile().filter((candidate) => {
16
22
  const state = readSessionState(candidate);
17
- return state !== null && isPidRunning(state.pid);
23
+ return state !== null && state.pid != null && isPidRunning(state.pid);
18
24
  });
19
25
  }
20
26
  function throwMissingSessionError() {
@@ -0,0 +1,8 @@
1
+ declare const TEST_ATTRIBUTE_NAMES: readonly ["data-testid", "data-test", "data-qa", "data-cy"];
2
+ declare const TRUSTED_ATTRIBUTE_NAMES: readonly ["id", "name", "for", "tabindex", "contenteditable", "role", "title", "alt", "type", "value", "placeholder", "autocomplete", "href", "action", "method", "src"];
3
+ declare const INTERACTIVE_TAG_NAMES: readonly ["a", "button", "input", "select", "textarea", "form", "details", "dialog", "label"];
4
+ declare const INTERACTIVE_ROLE_NAMES: readonly ["button", "link", "tab", "menuitem", "checkbox", "radio", "switch", "slider", "combobox"];
5
+ declare function filterSemanticClasses(value: string): string;
6
+ declare function isObfuscatedClass(cls: string): boolean;
7
+
8
+ export { INTERACTIVE_ROLE_NAMES, INTERACTIVE_TAG_NAMES, TEST_ATTRIBUTE_NAMES, TRUSTED_ATTRIBUTE_NAMES, filterSemanticClasses, isObfuscatedClass };
@@ -0,0 +1,69 @@
1
+ const TEST_ATTRIBUTE_NAMES = [
2
+ "data-testid",
3
+ "data-test",
4
+ "data-qa",
5
+ "data-cy"
6
+ ];
7
+ const TRUSTED_ATTRIBUTE_NAMES = [
8
+ "id",
9
+ "name",
10
+ "for",
11
+ "tabindex",
12
+ "contenteditable",
13
+ "role",
14
+ "title",
15
+ "alt",
16
+ "type",
17
+ "value",
18
+ "placeholder",
19
+ "autocomplete",
20
+ "href",
21
+ "action",
22
+ "method",
23
+ "src"
24
+ ];
25
+ const INTERACTIVE_TAG_NAMES = [
26
+ "a",
27
+ "button",
28
+ "input",
29
+ "select",
30
+ "textarea",
31
+ "form",
32
+ "details",
33
+ "dialog",
34
+ "label"
35
+ ];
36
+ const INTERACTIVE_ROLE_NAMES = [
37
+ "button",
38
+ "link",
39
+ "tab",
40
+ "menuitem",
41
+ "checkbox",
42
+ "radio",
43
+ "switch",
44
+ "slider",
45
+ "combobox"
46
+ ];
47
+ function filterSemanticClasses(value) {
48
+ const classes = value.split(/\s+/).filter(Boolean);
49
+ const kept = classes.filter((cls) => !isObfuscatedClass(cls));
50
+ return kept.join(" ");
51
+ }
52
+ function isObfuscatedClass(cls) {
53
+ if (cls.length > 80) return true;
54
+ if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
55
+ if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
56
+ if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
57
+ const digits = (cls.match(/[0-9]/g) || []).length;
58
+ const letters = (cls.match(/[a-zA-Z]/g) || []).length;
59
+ if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
60
+ return false;
61
+ }
62
+ export {
63
+ INTERACTIVE_ROLE_NAMES,
64
+ INTERACTIVE_TAG_NAMES,
65
+ TEST_ATTRIBUTE_NAMES,
66
+ TRUSTED_ATTRIBUTE_NAMES,
67
+ filterSemanticClasses,
68
+ isObfuscatedClass
69
+ };
@@ -25,6 +25,7 @@ const LOCATOR_ACTIONS = [
25
25
  ];
26
26
  const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"];
27
27
  const POINTER_ACTIONS = /* @__PURE__ */ new Set(["click", "dblclick", "hover"]);
28
+ const instrumentedTargets = /* @__PURE__ */ new WeakSet();
28
29
  const pageQueues = /* @__PURE__ */ new WeakMap();
29
30
  function enqueue(page, fn) {
30
31
  const prev = pageQueues.get(page) ?? Promise.resolve();
@@ -96,11 +97,93 @@ function wrapLocatorActions(locator, page, opts) {
96
97
  };
97
98
  }
98
99
  }
100
+ const LOCATOR_FACTORY_METHODS = [
101
+ "locator",
102
+ "getByRole",
103
+ "getByText",
104
+ "getByLabel",
105
+ "getByPlaceholder",
106
+ "getByAltText",
107
+ "getByTitle",
108
+ "getByTestId",
109
+ "filter",
110
+ "and",
111
+ "or",
112
+ "first",
113
+ "last",
114
+ "nth"
115
+ ];
116
+ const FRAME_LOCATOR_FACTORY_METHODS = [
117
+ "locator",
118
+ "getByRole",
119
+ "getByText",
120
+ "getByLabel",
121
+ "getByPlaceholder",
122
+ "getByAltText",
123
+ "getByTitle",
124
+ "getByTestId",
125
+ "owner",
126
+ "first",
127
+ "last",
128
+ "nth"
129
+ ];
130
+ function instrumentLocator(locator, page, opts) {
131
+ const target = locator;
132
+ if (instrumentedTargets.has(target)) {
133
+ return locator;
134
+ }
135
+ instrumentedTargets.add(target);
136
+ wrapLocatorActions(locator, page, opts);
137
+ for (const method of LOCATOR_FACTORY_METHODS) {
138
+ if (typeof locator[method] !== "function") continue;
139
+ const orig = locator[method].bind(locator);
140
+ locator[method] = (...args) => {
141
+ const nextLocator = orig(...args);
142
+ return instrumentLocator(nextLocator, page, opts);
143
+ };
144
+ }
145
+ if (typeof locator.contentFrame === "function") {
146
+ const origContentFrame = locator.contentFrame.bind(locator);
147
+ locator.contentFrame = (...args) => {
148
+ const frameLocator = origContentFrame(...args);
149
+ return instrumentFrameLocator(frameLocator, page, opts);
150
+ };
151
+ }
152
+ return locator;
153
+ }
154
+ function instrumentFrameLocator(frameLocator, page, opts) {
155
+ const target = frameLocator;
156
+ if (instrumentedTargets.has(target)) {
157
+ return frameLocator;
158
+ }
159
+ instrumentedTargets.add(target);
160
+ for (const method of FRAME_LOCATOR_FACTORY_METHODS) {
161
+ if (typeof frameLocator[method] !== "function") continue;
162
+ const orig = frameLocator[method].bind(frameLocator);
163
+ frameLocator[method] = (...args) => {
164
+ const result = orig(...args);
165
+ if (method === "first" || method === "last" || method === "nth") {
166
+ return instrumentFrameLocator(result, page, opts);
167
+ }
168
+ return instrumentLocator(result, page, opts);
169
+ };
170
+ }
171
+ if (typeof frameLocator.frameLocator === "function") {
172
+ const origFrameLocator = frameLocator.frameLocator.bind(
173
+ frameLocator
174
+ );
175
+ frameLocator.frameLocator = (...args) => {
176
+ const nestedFrameLocator = origFrameLocator(...args);
177
+ return instrumentFrameLocator(nestedFrameLocator, page, opts);
178
+ };
179
+ }
180
+ return frameLocator;
181
+ }
99
182
  function isTimeoutError(err) {
100
183
  if (!err || typeof err.message !== "string") return false;
101
184
  return err.message.includes("Timeout") || err.message.includes("timeout") || err.name === "TimeoutError";
102
185
  }
103
- const LOCATOR_FACTORIES = [
186
+ const PAGE_LOCATOR_FACTORIES = [
104
187
  "locator",
105
188
  "getByRole",
106
189
  "getByText",
@@ -110,6 +193,7 @@ const LOCATOR_FACTORIES = [
110
193
  "getByTitle",
111
194
  "getByTestId"
112
195
  ];
196
+ const PAGE_FRAME_LOCATOR_FACTORIES = ["frameLocator"];
113
197
  async function installInstrumentation(page, options) {
114
198
  if (page.__librettoInstrumented) return;
115
199
  page.__librettoInstrumented = true;
@@ -129,7 +213,12 @@ async function installInstrumentation(page, options) {
129
213
  try {
130
214
  const loc = page.locator(args[0]);
131
215
  const box = await loc.boundingBox();
132
- await visualizeBeforeAction(page, box, method, highlightBeforeActionMs);
216
+ await visualizeBeforeAction(
217
+ page,
218
+ box,
219
+ method,
220
+ highlightBeforeActionMs
221
+ );
133
222
  } catch {
134
223
  }
135
224
  });
@@ -161,13 +250,20 @@ async function installInstrumentation(page, options) {
161
250
  return orig(...args);
162
251
  };
163
252
  }
164
- for (const factory of LOCATOR_FACTORIES) {
253
+ for (const factory of PAGE_LOCATOR_FACTORIES) {
165
254
  if (typeof page[factory] !== "function") continue;
166
255
  const origFactory = page[factory].bind(page);
167
256
  page[factory] = (...factoryArgs) => {
168
257
  const locator = origFactory(...factoryArgs);
169
- wrapLocatorActions(locator, page, mergedOpts);
170
- return locator;
258
+ return instrumentLocator(locator, page, mergedOpts);
259
+ };
260
+ }
261
+ for (const factory of PAGE_FRAME_LOCATOR_FACTORIES) {
262
+ if (typeof page[factory] !== "function") continue;
263
+ const origFactory = page[factory].bind(page);
264
+ page[factory] = (...factoryArgs) => {
265
+ const frameLocator = origFactory(...factoryArgs);
266
+ return instrumentFrameLocator(frameLocator, page, mergedOpts);
171
267
  };
172
268
  }
173
269
  }
@@ -18,7 +18,9 @@ function createLLMClientFromModel(model) {
18
18
  if (msg.role === "assistant") {
19
19
  return {
20
20
  role: "assistant",
21
- content: msg.content.filter((part) => part.type === "text").map((part) => ({ type: "text", text: part.text }))
21
+ content: msg.content.filter(
22
+ (part) => part.type === "text"
23
+ ).map((part) => ({ type: "text", text: part.text }))
22
24
  };
23
25
  }
24
26
  return {
@@ -121,7 +121,9 @@ function convertUserContentParts(parts) {
121
121
  });
122
122
  }
123
123
  function convertAssistantContentParts(parts) {
124
- return parts.filter((part) => part.type === "text").map((part) => ({ type: "text", text: part.text }));
124
+ return parts.filter(
125
+ (part) => part.type === "text"
126
+ ).map((part) => ({ type: "text", text: part.text }));
125
127
  }
126
128
  function convertMessages(messages) {
127
129
  return messages.map((msg) => {
@@ -1,4 +1,7 @@
1
- import { Logger, defaultLogger } from "./logger.js";
1
+ import {
2
+ Logger,
3
+ defaultLogger
4
+ } from "./logger.js";
2
5
  import {
3
6
  createFileLogSink,
4
7
  prettyConsoleSink,
@@ -1,4 +1,6 @@
1
- import { launchBrowser } from "./browser.js";
1
+ import {
2
+ launchBrowser
3
+ } from "./browser.js";
2
4
  export {
3
5
  launchBrowser
4
6
  };