libretto 0.4.4 → 0.5.0

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 (152) hide show
  1. package/dist/cli/cli.js +20 -19
  2. package/dist/cli/commands/ai.js +1 -1
  3. package/dist/cli/commands/browser.js +3 -3
  4. package/dist/cli/commands/execution.js +3 -3
  5. package/dist/cli/commands/logs.js +1 -1
  6. package/dist/cli/core/browser.js +11 -6
  7. package/dist/cli/core/context.js +4 -18
  8. package/dist/cli/core/session.js +2 -2
  9. package/dist/cli/core/snapshot-analyzer.js +2 -2
  10. package/dist/cli/router.js +1 -1
  11. package/dist/cli/workers/run-integration-runtime.js +2 -2
  12. package/dist/shared/paths/paths.js +2 -1
  13. package/dist/shared/paths/repo-root.d.ts +3 -0
  14. package/dist/shared/paths/repo-root.js +24 -0
  15. package/package.json +6 -7
  16. package/scripts/postinstall.mjs +12 -3
  17. package/skills/libretto/SKILL.md +93 -404
  18. package/skills/libretto/references/auth-profiles.md +30 -0
  19. package/skills/libretto/references/pages-and-page-targeting.md +29 -0
  20. package/skills/libretto/references/reverse-engineering-network-requests.md +39 -0
  21. package/skills/libretto/references/user-action-log.md +31 -0
  22. package/src/cli/cli.ts +173 -0
  23. package/src/cli/commands/ai.ts +35 -0
  24. package/src/cli/commands/browser.ts +165 -0
  25. package/src/cli/commands/execution.ts +691 -0
  26. package/src/cli/commands/init.ts +327 -0
  27. package/src/cli/commands/logs.ts +128 -0
  28. package/src/cli/commands/shared.ts +70 -0
  29. package/src/cli/commands/snapshot.ts +327 -0
  30. package/src/cli/core/ai-config.ts +255 -0
  31. package/src/cli/core/api-snapshot-analyzer.ts +97 -0
  32. package/src/cli/core/browser.ts +839 -0
  33. package/src/cli/core/context.ts +122 -0
  34. package/src/cli/core/pause-signals.ts +35 -0
  35. package/src/cli/core/session-telemetry.ts +553 -0
  36. package/src/cli/core/session.ts +209 -0
  37. package/src/cli/core/snapshot-analyzer.ts +875 -0
  38. package/src/cli/core/snapshot-api-config.ts +236 -0
  39. package/src/cli/core/telemetry.ts +446 -0
  40. package/src/cli/framework/simple-cli.ts +1273 -0
  41. package/src/cli/index.ts +13 -0
  42. package/src/cli/router.ts +28 -0
  43. package/src/cli/workers/run-integration-runtime.ts +311 -0
  44. package/src/cli/workers/run-integration-worker-protocol.ts +14 -0
  45. package/src/cli/workers/run-integration-worker.ts +75 -0
  46. package/src/index.ts +120 -0
  47. package/src/runtime/download/download.ts +100 -0
  48. package/src/runtime/download/index.ts +7 -0
  49. package/src/runtime/extract/extract.ts +92 -0
  50. package/src/runtime/extract/index.ts +1 -0
  51. package/src/runtime/network/index.ts +5 -0
  52. package/src/runtime/network/network.ts +113 -0
  53. package/src/runtime/recovery/agent.ts +256 -0
  54. package/src/runtime/recovery/errors.ts +152 -0
  55. package/src/runtime/recovery/index.ts +7 -0
  56. package/src/runtime/recovery/recovery.ts +50 -0
  57. package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +243 -115
  58. package/src/shared/config/config.ts +22 -0
  59. package/src/shared/config/index.ts +5 -0
  60. package/src/shared/debug/index.ts +1 -0
  61. package/src/shared/debug/pause.ts +85 -0
  62. package/src/shared/instrumentation/errors.ts +82 -0
  63. package/src/shared/instrumentation/index.ts +9 -0
  64. package/src/shared/instrumentation/instrument.ts +276 -0
  65. package/src/shared/llm/ai-sdk-adapter.ts +78 -0
  66. package/src/shared/llm/client.ts +217 -0
  67. package/src/shared/llm/index.ts +3 -0
  68. package/src/shared/llm/types.ts +63 -0
  69. package/src/shared/logger/index.ts +6 -0
  70. package/src/shared/logger/logger.ts +352 -0
  71. package/src/shared/logger/sinks.ts +144 -0
  72. package/src/shared/paths/paths.ts +109 -0
  73. package/src/shared/paths/repo-root.ts +27 -0
  74. package/src/shared/run/api.ts +2 -0
  75. package/src/shared/run/browser.ts +98 -0
  76. package/src/shared/state/index.ts +11 -0
  77. package/src/shared/state/session-state.ts +74 -0
  78. package/src/shared/visualization/ghost-cursor.ts +200 -0
  79. package/src/shared/visualization/highlight.ts +146 -0
  80. package/src/shared/visualization/index.ts +18 -0
  81. package/src/shared/workflow/workflow.ts +42 -0
  82. package/dist/index.cjs +0 -144
  83. package/dist/index.d.cts +0 -21
  84. package/dist/runtime/download/download.cjs +0 -70
  85. package/dist/runtime/download/download.d.cts +0 -35
  86. package/dist/runtime/download/index.cjs +0 -30
  87. package/dist/runtime/download/index.d.cts +0 -3
  88. package/dist/runtime/extract/extract.cjs +0 -88
  89. package/dist/runtime/extract/extract.d.cts +0 -23
  90. package/dist/runtime/extract/index.cjs +0 -28
  91. package/dist/runtime/extract/index.d.cts +0 -5
  92. package/dist/runtime/network/index.cjs +0 -28
  93. package/dist/runtime/network/index.d.cts +0 -4
  94. package/dist/runtime/network/network.cjs +0 -91
  95. package/dist/runtime/network/network.d.cts +0 -28
  96. package/dist/runtime/recovery/agent.cjs +0 -223
  97. package/dist/runtime/recovery/agent.d.cts +0 -13
  98. package/dist/runtime/recovery/errors.cjs +0 -124
  99. package/dist/runtime/recovery/errors.d.cts +0 -31
  100. package/dist/runtime/recovery/index.cjs +0 -34
  101. package/dist/runtime/recovery/index.d.cts +0 -7
  102. package/dist/runtime/recovery/recovery.cjs +0 -55
  103. package/dist/runtime/recovery/recovery.d.cts +0 -12
  104. package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
  105. package/dist/shared/config/config.cjs +0 -44
  106. package/dist/shared/config/config.d.cts +0 -10
  107. package/dist/shared/config/index.cjs +0 -32
  108. package/dist/shared/config/index.d.cts +0 -1
  109. package/dist/shared/debug/index.cjs +0 -28
  110. package/dist/shared/debug/index.d.cts +0 -1
  111. package/dist/shared/debug/pause.cjs +0 -86
  112. package/dist/shared/debug/pause.d.cts +0 -12
  113. package/dist/shared/instrumentation/errors.cjs +0 -81
  114. package/dist/shared/instrumentation/errors.d.cts +0 -12
  115. package/dist/shared/instrumentation/index.cjs +0 -35
  116. package/dist/shared/instrumentation/index.d.cts +0 -6
  117. package/dist/shared/instrumentation/instrument.cjs +0 -206
  118. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  119. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
  120. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  121. package/dist/shared/llm/client.cjs +0 -218
  122. package/dist/shared/llm/client.d.cts +0 -13
  123. package/dist/shared/llm/index.cjs +0 -31
  124. package/dist/shared/llm/index.d.cts +0 -5
  125. package/dist/shared/llm/types.cjs +0 -16
  126. package/dist/shared/llm/types.d.cts +0 -67
  127. package/dist/shared/logger/index.cjs +0 -37
  128. package/dist/shared/logger/index.d.cts +0 -2
  129. package/dist/shared/logger/logger.cjs +0 -232
  130. package/dist/shared/logger/logger.d.cts +0 -86
  131. package/dist/shared/logger/sinks.cjs +0 -160
  132. package/dist/shared/logger/sinks.d.cts +0 -9
  133. package/dist/shared/paths/paths.cjs +0 -104
  134. package/dist/shared/paths/paths.d.cts +0 -10
  135. package/dist/shared/run/api.cjs +0 -28
  136. package/dist/shared/run/api.d.cts +0 -2
  137. package/dist/shared/run/browser.cjs +0 -98
  138. package/dist/shared/run/browser.d.cts +0 -22
  139. package/dist/shared/state/index.cjs +0 -38
  140. package/dist/shared/state/index.d.cts +0 -2
  141. package/dist/shared/state/session-state.cjs +0 -92
  142. package/dist/shared/state/session-state.d.cts +0 -40
  143. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  144. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  145. package/dist/shared/visualization/highlight.cjs +0 -134
  146. package/dist/shared/visualization/highlight.d.cts +0 -22
  147. package/dist/shared/visualization/index.cjs +0 -45
  148. package/dist/shared/visualization/index.d.cts +0 -3
  149. package/dist/shared/workflow/workflow.cjs +0 -47
  150. package/dist/shared/workflow/workflow.d.cts +0 -21
  151. package/skills/libretto/code-generation-rules.md +0 -223
  152. package/skills/libretto/integration-approach-selection.md +0 -174
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Runtime configuration for libretto.
3
+ *
4
+ * Values are derived from environment variables only.
5
+ */
6
+
7
+ export function isDebugMode(): boolean {
8
+ return process.env.LIBRETTO_DEBUG === "true";
9
+ }
10
+
11
+ export function isDryRun(): boolean {
12
+ const explicit = process.env.LIBRETTO_DRY_RUN;
13
+ if (explicit !== undefined) {
14
+ return explicit === "true";
15
+ }
16
+
17
+ return process.env.NODE_ENV === "development";
18
+ }
19
+
20
+ export function shouldPauseBeforeMutation(): boolean {
21
+ return isDryRun() && isDebugMode();
22
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ isDebugMode,
3
+ isDryRun,
4
+ shouldPauseBeforeMutation,
5
+ } from "./config.js";
@@ -0,0 +1 @@
1
+ export { pause } from "./pause.js";
@@ -0,0 +1,85 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
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";
6
+
7
+ function isPidRunning(pid: number): boolean {
8
+ try {
9
+ process.kill(pid, 0);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ function getRunningSessions(): string[] {
17
+ return listSessionsWithStateFile().filter((candidate) => {
18
+ const state = readSessionState(candidate);
19
+ return state !== null && isPidRunning(state.pid);
20
+ });
21
+ }
22
+
23
+ function throwMissingSessionError(): never {
24
+ const runningSessions = getRunningSessions();
25
+ const lines = ["pause(session) requires a non-empty session ID."];
26
+
27
+ if (runningSessions.length > 0) {
28
+ lines.push("", "Running sessions:");
29
+ for (const runningSession of runningSessions) {
30
+ lines.push(` ${runningSession}`);
31
+ }
32
+ }
33
+
34
+ throw new Error(lines.join("\n"));
35
+ }
36
+
37
+ /**
38
+ * Standalone pause function.
39
+ *
40
+ * - In production (`NODE_ENV === "production"`), returns immediately (no-op).
41
+ * - Otherwise, writes a `.paused` signal file and polls for a `.resume` signal,
42
+ * using the same file-based mechanism as the CLI runner.
43
+ *
44
+ * Import directly: `import { pause } from "libretto";`
45
+ */
46
+ export async function pause(session: string): Promise<void> {
47
+ if (process.env.NODE_ENV === "production") {
48
+ return;
49
+ }
50
+
51
+ if (typeof session !== "string" || session.trim().length === 0) {
52
+ throwMissingSessionError();
53
+ }
54
+
55
+ const signalPaths = getPauseSignalPaths(session);
56
+ const { pausedSignalPath, resumeSignalPath } = signalPaths;
57
+
58
+ await mkdir(getSessionDir(session), { recursive: true });
59
+ await removeSignalIfExists(resumeSignalPath);
60
+
61
+ const details = {
62
+ sessionName: session,
63
+ pausedAt: new Date().toISOString(),
64
+ url: "unknown",
65
+ };
66
+
67
+ // Try to read the current page URL from the process (best-effort).
68
+ // The standalone pause doesn't have access to the page object,
69
+ // so we just record what we can.
70
+ await writeFile(pausedSignalPath, JSON.stringify(details, null, 2), "utf8");
71
+
72
+ console.log(`[pause] Paused (session: ${session})`);
73
+ console.log("[pause] Waiting for resume signal...");
74
+
75
+ const RESUME_POLL_INTERVAL_MS = 250;
76
+ while (!existsSync(resumeSignalPath)) {
77
+ await new Promise((resolve) =>
78
+ setTimeout(resolve, RESUME_POLL_INTERVAL_MS),
79
+ );
80
+ }
81
+
82
+ await removeSignalIfExists(resumeSignalPath);
83
+ await removeSignalIfExists(pausedSignalPath);
84
+ console.log("[pause] Resume signal received. Continuing workflow...");
85
+ }
@@ -0,0 +1,82 @@
1
+ import type { Page, Locator } from "playwright";
2
+
3
+ /**
4
+ * Enrich a timeout error from a pointer action (click/dblclick/hover) with
5
+ * diagnostic information about why the action may have failed.
6
+ *
7
+ * Mutates err.message in-place to append the enrichment.
8
+ * Best-effort: if any probe fails, we skip that check silently.
9
+ */
10
+ export async function enrichTimeoutError(
11
+ err: any,
12
+ locator: Locator,
13
+ page: Page,
14
+ ): Promise<void> {
15
+ const reasons: string[] = [];
16
+
17
+ try {
18
+ const visible = await locator.isVisible().catch(() => null);
19
+ if (visible === false) {
20
+ reasons.push("Element is not visible");
21
+ }
22
+
23
+ // isInViewport is available in modern Playwright but may not exist in older versions
24
+ if (typeof (locator as any).isInViewport === "function") {
25
+ const inViewport = await (locator as any).isInViewport().catch(() => null);
26
+ if (inViewport === false) {
27
+ reasons.push("Element is outside of the viewport");
28
+ }
29
+ }
30
+
31
+ const enabled = await locator.isEnabled().catch(() => null);
32
+ if (enabled === false) {
33
+ reasons.push("Element is not enabled (disabled)");
34
+ }
35
+
36
+ // If the element appears visible and in viewport, check for intercepting elements
37
+ if (reasons.length === 0) {
38
+ const box = await locator.boundingBox().catch(() => null);
39
+ if (box) {
40
+ const centerX = box.x + box.width / 2;
41
+ const centerY = box.y + box.height / 2;
42
+
43
+ const interceptInfo = await page
44
+ .evaluate(
45
+ ({ x, y }) => {
46
+ const els = document.elementsFromPoint(x, y);
47
+ if (!els || els.length < 2) return null;
48
+ const topEl = els[0];
49
+ if (!topEl) return null;
50
+
51
+ // Build a brief preview of the intercepting element
52
+ const tag = topEl.tagName.toLowerCase();
53
+ const id = topEl.id ? `#${topEl.id}` : "";
54
+ const cls = topEl.className
55
+ ? `.${String(topEl.className).split(/\s+/).slice(0, 2).join(".")}`
56
+ : "";
57
+ const text = (topEl.textContent || "").trim().slice(0, 40);
58
+ return {
59
+ tag,
60
+ preview: `<${tag}${id}${cls}>${text ? ` "${text}"` : ""}`,
61
+ };
62
+ },
63
+ { x: centerX, y: centerY },
64
+ )
65
+ .catch(() => null);
66
+
67
+ if (interceptInfo) {
68
+ reasons.push(
69
+ `Element may be intercepted by ${interceptInfo.preview}`,
70
+ );
71
+ }
72
+ }
73
+ }
74
+ } catch {
75
+ // All enrichment is best-effort
76
+ }
77
+
78
+ if (reasons.length > 0) {
79
+ const enrichment = `\n[libretto diagnostics] ${reasons.join("; ")}`;
80
+ err.message = (err.message || "") + enrichment;
81
+ }
82
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ instrumentPage,
3
+ installInstrumentation,
4
+ instrumentContext,
5
+ type InstrumentationOptions,
6
+ type InstrumentedPage,
7
+ } from "./instrument.js";
8
+
9
+ export { enrichTimeoutError } from "./errors.js";
@@ -0,0 +1,276 @@
1
+ import type { Page, Locator, BrowserContext } from "playwright";
2
+ import type { MinimalLogger } from "../logger/logger.js";
3
+ import type { GhostCursorOptions } from "../visualization/ghost-cursor.js";
4
+ import type { HighlightOptions } from "../visualization/highlight.js";
5
+ import {
6
+ ensureGhostCursor,
7
+ moveGhostCursor,
8
+ moveGhostCursorWithDistance,
9
+ ghostClick,
10
+ getGhostCursorPosition,
11
+ } from "../visualization/ghost-cursor.js";
12
+ import {
13
+ ensureHighlightLayer,
14
+ showHighlight,
15
+ clearHighlights,
16
+ } from "../visualization/highlight.js";
17
+ import { enrichTimeoutError } from "./errors.js";
18
+
19
+ export type InstrumentationOptions = {
20
+ visualize?: boolean;
21
+ logger?: MinimalLogger;
22
+ highlightBeforeActionMs?: number;
23
+ ghostCursor?: GhostCursorOptions;
24
+ highlight?: HighlightOptions;
25
+ };
26
+
27
+ export type InstrumentedPage = Page & {
28
+ __librettoInstrumented: true;
29
+ };
30
+
31
+ const LOCATOR_ACTIONS = [
32
+ "click",
33
+ "dblclick",
34
+ "hover",
35
+ "fill",
36
+ "type",
37
+ "press",
38
+ "check",
39
+ "uncheck",
40
+ "selectOption",
41
+ "focus",
42
+ ] as const;
43
+
44
+ const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"] as const;
45
+
46
+ const POINTER_ACTIONS = new Set<string>(["click", "dblclick", "hover"]);
47
+
48
+ // Per-page serialization queue so overlapping visualization actions don't glitch
49
+ const pageQueues = new WeakMap<Page, Promise<void>>();
50
+
51
+ function enqueue(page: Page, fn: () => Promise<void>): Promise<void> {
52
+ const prev = pageQueues.get(page) ?? Promise.resolve();
53
+ const next = prev.then(fn, fn);
54
+ pageQueues.set(page, next);
55
+ return next;
56
+ }
57
+
58
+ async function visualizeBeforeAction(
59
+ page: Page,
60
+ box: { x: number; y: number; width: number; height: number } | null,
61
+ actionName: string,
62
+ highlightMs: number,
63
+ ): Promise<void> {
64
+ if (!box) return;
65
+
66
+ // Re-ensure overlays in case DOM was replaced (e.g. page.setContent()).
67
+ await ensureGhostCursor(page);
68
+ await ensureHighlightLayer(page);
69
+
70
+ const centerX = box.x + box.width / 2;
71
+ const centerY = box.y + box.height / 2;
72
+
73
+ // Show highlight on the target element
74
+ await showHighlight(page, {
75
+ box,
76
+ durationMs: highlightMs + 200, // keep visible a bit past the cursor arrival
77
+ });
78
+
79
+ // Move ghost cursor to target
80
+ const cursorPos = await getGhostCursorPosition(page);
81
+ if (cursorPos) {
82
+ await moveGhostCursorWithDistance(page, cursorPos, {
83
+ x: centerX,
84
+ y: centerY,
85
+ });
86
+ } else {
87
+ await moveGhostCursor(page, { x: centerX, y: centerY, durationMs: 200 });
88
+ }
89
+
90
+ // For click actions, show click feedback
91
+ if (actionName === "click" || actionName === "dblclick") {
92
+ await ghostClick(page, { x: centerX, y: centerY });
93
+ }
94
+ }
95
+
96
+ async function visualizeAfterAction(page: Page): Promise<void> {
97
+ await clearHighlights(page);
98
+ }
99
+
100
+ function wrapLocatorActions(
101
+ locator: Locator,
102
+ page: Page,
103
+ opts: Required<Pick<InstrumentationOptions, "visualize" | "highlightBeforeActionMs">> & InstrumentationOptions,
104
+ ): void {
105
+ for (const method of LOCATOR_ACTIONS) {
106
+ if (typeof (locator as any)[method] !== "function") continue;
107
+ const orig = (locator as any)[method].bind(locator);
108
+
109
+ (locator as any)[method] = async (...args: any[]) => {
110
+ if (opts.visualize) {
111
+ await enqueue(page, async () => {
112
+ try {
113
+ const box = await locator.boundingBox();
114
+ await visualizeBeforeAction(
115
+ page,
116
+ box,
117
+ method,
118
+ opts.highlightBeforeActionMs,
119
+ );
120
+ } catch {
121
+ // Best-effort visualization
122
+ }
123
+ });
124
+ }
125
+
126
+ try {
127
+ const result = await orig(...args);
128
+ if (opts.visualize) {
129
+ enqueue(page, () => visualizeAfterAction(page));
130
+ }
131
+ return result;
132
+ } catch (err: any) {
133
+ if (opts.visualize) {
134
+ enqueue(page, () => visualizeAfterAction(page));
135
+ }
136
+ // Enrich timeout errors for pointer actions
137
+ if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
138
+ await enrichTimeoutError(err, locator, page);
139
+ }
140
+ throw err;
141
+ }
142
+ };
143
+ }
144
+ }
145
+
146
+ function isTimeoutError(err: any): boolean {
147
+ if (!err || typeof err.message !== "string") return false;
148
+ return (
149
+ err.message.includes("Timeout") ||
150
+ err.message.includes("timeout") ||
151
+ err.name === "TimeoutError"
152
+ );
153
+ }
154
+
155
+ const LOCATOR_FACTORIES = [
156
+ "locator",
157
+ "getByRole",
158
+ "getByText",
159
+ "getByLabel",
160
+ "getByPlaceholder",
161
+ "getByAltText",
162
+ "getByTitle",
163
+ "getByTestId",
164
+ ] as const;
165
+
166
+ /**
167
+ * In-place patching of a Page object to add visualization and error enrichment.
168
+ * Modifies the page directly (does not return a new object).
169
+ */
170
+ export async function installInstrumentation(
171
+ page: Page,
172
+ options?: InstrumentationOptions,
173
+ ): Promise<void> {
174
+ if ((page as any).__librettoInstrumented) return;
175
+ (page as any).__librettoInstrumented = true;
176
+
177
+ const visualize = options?.visualize ?? false;
178
+ const highlightBeforeActionMs = options?.highlightBeforeActionMs ?? 350;
179
+ const mergedOpts = { ...options, visualize, highlightBeforeActionMs };
180
+
181
+ // Install overlay layers if visualization is on
182
+ if (visualize) {
183
+ await ensureGhostCursor(page, options?.ghostCursor);
184
+ await ensureHighlightLayer(page, options?.highlight);
185
+ }
186
+
187
+ // Wrap page-level locator actions (page.click, page.fill, etc.)
188
+ for (const method of LOCATOR_ACTIONS) {
189
+ if (typeof (page as any)[method] !== "function") continue;
190
+ const orig = (page as any)[method].bind(page);
191
+ (page as any)[method] = async (...args: any[]) => {
192
+ // For page-level actions, the first arg is typically the selector
193
+ if (visualize && typeof args[0] === "string") {
194
+ await enqueue(page, async () => {
195
+ try {
196
+ const loc = page.locator(args[0]);
197
+ const box = await loc.boundingBox();
198
+ await visualizeBeforeAction(page, box, method, highlightBeforeActionMs);
199
+ } catch {
200
+ // Best-effort
201
+ }
202
+ });
203
+ }
204
+
205
+ try {
206
+ const result = await orig(...args);
207
+ if (visualize) {
208
+ enqueue(page, () => visualizeAfterAction(page));
209
+ }
210
+ return result;
211
+ } catch (err: any) {
212
+ if (visualize) {
213
+ enqueue(page, () => visualizeAfterAction(page));
214
+ }
215
+ if (POINTER_ACTIONS.has(method) && isTimeoutError(err) && typeof args[0] === "string") {
216
+ await enrichTimeoutError(err, page.locator(args[0]), page);
217
+ }
218
+ throw err;
219
+ }
220
+ };
221
+ }
222
+
223
+ // Wrap navigation actions (no visualization, just logging)
224
+ for (const method of NAV_ACTIONS) {
225
+ if (typeof (page as any)[method] !== "function") continue;
226
+ const orig = (page as any)[method].bind(page);
227
+ (page as any)[method] = async (...args: any[]) => {
228
+ options?.logger?.info(`instrumentation:${method}`, {
229
+ url: typeof args[0] === "string" ? args[0] : page.url(),
230
+ });
231
+ return orig(...args);
232
+ };
233
+ }
234
+
235
+ // Wrap locator factories to instrument returned locators
236
+ for (const factory of LOCATOR_FACTORIES) {
237
+ if (typeof (page as any)[factory] !== "function") continue;
238
+ const origFactory = (page as any)[factory].bind(page);
239
+ (page as any)[factory] = (...factoryArgs: any[]) => {
240
+ const locator = origFactory(...factoryArgs);
241
+ wrapLocatorActions(locator, page, mergedOpts);
242
+ return locator;
243
+ };
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Returns a new object that proxies to the page with instrumentation applied.
249
+ * The original page is not modified.
250
+ */
251
+ export async function instrumentPage(
252
+ page: Page,
253
+ options?: InstrumentationOptions,
254
+ ): Promise<InstrumentedPage> {
255
+ await installInstrumentation(page, options);
256
+ return page as InstrumentedPage;
257
+ }
258
+
259
+ /**
260
+ * Install overlays on a page and auto-install on all future pages in the context.
261
+ * Useful when connecting to an existing browser via CDP.
262
+ */
263
+ export async function instrumentContext(
264
+ context: BrowserContext,
265
+ options?: InstrumentationOptions,
266
+ ): Promise<void> {
267
+ // Instrument all existing pages
268
+ for (const page of context.pages()) {
269
+ await installInstrumentation(page, options);
270
+ }
271
+
272
+ // Auto-instrument new pages
273
+ context.on("page", async (page) => {
274
+ await installInstrumentation(page, options);
275
+ });
276
+ }
@@ -0,0 +1,78 @@
1
+ import { generateObject, type LanguageModel } from "ai";
2
+ import type { ZodType, output as ZodOutput } from "zod";
3
+ import type { LLMClient, Message } from "./types.js";
4
+
5
+ /**
6
+ * Creates a libretto LLMClient from a Vercel AI SDK LanguageModel.
7
+ *
8
+ * This eliminates the need for consumers to write their own adapter
9
+ * when using @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/google-vertex,
10
+ * or any other Vercel AI SDK-compatible provider.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createLLMClientFromModel } from "libretto/llm";
15
+ * import { openai } from "@ai-sdk/openai";
16
+ *
17
+ * const llmClient = createLLMClientFromModel(openai("gpt-4o"));
18
+ * ```
19
+ */
20
+ export function createLLMClientFromModel(model: LanguageModel): LLMClient {
21
+ return {
22
+ async generateObject<T extends ZodType>(opts: {
23
+ prompt: string;
24
+ schema: T;
25
+ temperature?: number;
26
+ }): Promise<ZodOutput<T>> {
27
+ const { object } = await generateObject({
28
+ model,
29
+ schema: opts.schema,
30
+ prompt: opts.prompt,
31
+ temperature: opts.temperature ?? 0,
32
+ });
33
+ return object as ZodOutput<T>;
34
+ },
35
+
36
+ async generateObjectFromMessages<T extends ZodType>(opts: {
37
+ messages: Message[];
38
+ schema: T;
39
+ temperature?: number;
40
+ }): Promise<ZodOutput<T>> {
41
+ // Convert libretto Message format to AI SDK message format
42
+ const messages = opts.messages.map((msg) => {
43
+ if (typeof msg.content === "string") {
44
+ return { role: msg.role, content: msg.content };
45
+ }
46
+ if (msg.role === "assistant") {
47
+ // AssistantContent only supports text parts (no images)
48
+ return {
49
+ role: "assistant" as const,
50
+ content: msg.content
51
+ .filter((part): part is typeof part & { type: "text" } => part.type === "text")
52
+ .map((part) => ({ type: "text" as const, text: part.text })),
53
+ };
54
+ }
55
+ return {
56
+ role: "user" as const,
57
+ content: msg.content.map((part) =>
58
+ part.type === "text"
59
+ ? { type: "text" as const, text: part.text }
60
+ : {
61
+ type: "image" as const,
62
+ image: part.image,
63
+ ...(part.mediaType ? { mediaType: part.mediaType } : {}),
64
+ },
65
+ ),
66
+ };
67
+ });
68
+
69
+ const { object } = await generateObject({
70
+ model,
71
+ schema: opts.schema,
72
+ messages,
73
+ temperature: opts.temperature ?? 0,
74
+ });
75
+ return object as ZodOutput<T>;
76
+ },
77
+ };
78
+ }