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,122 @@
1
+ import { Logger, createFileLogSink } from "../../shared/logger/index.js";
2
+ import type { LLMClient } from "../../shared/llm/index.js";
3
+ import type { LoggerApi } from "../../shared/logger/index.js";
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
7
+ import { validateSessionName } from "./session.js";
8
+
9
+ export const REPO_ROOT = resolveLibrettoRepoRoot();
10
+ export const LIBRETTO_CONFIG_DIR = join(REPO_ROOT, ".libretto");
11
+ export const LIBRETTO_CONFIG_PATH = join(LIBRETTO_CONFIG_DIR, "config.json");
12
+ export const PROFILES_DIR = join(LIBRETTO_CONFIG_DIR, "profiles");
13
+ export const LIBRETTO_SESSIONS_DIR = join(LIBRETTO_CONFIG_DIR, "sessions");
14
+ export const LIBRETTO_GITIGNORE_PATH = join(LIBRETTO_CONFIG_DIR, ".gitignore");
15
+
16
+ const LIBRETTO_GITIGNORE_CONTENT = [
17
+ "# Local libretto runtime state",
18
+ "sessions/",
19
+ "profiles/",
20
+ "",
21
+ ].join("\n");
22
+
23
+ export function getSessionDir(session: string): string {
24
+ return join(LIBRETTO_SESSIONS_DIR, session);
25
+ }
26
+
27
+ export function getSessionStatePath(session: string): string {
28
+ return join(getSessionDir(session), "state.json");
29
+ }
30
+
31
+ export function getSessionLogsPath(session: string): string {
32
+ return join(getSessionDir(session), "logs.jsonl");
33
+ }
34
+
35
+ export function getSessionNetworkLogPath(session: string): string {
36
+ return join(getSessionDir(session), "network.jsonl");
37
+ }
38
+
39
+ export function getSessionActionsLogPath(session: string): string {
40
+ return join(getSessionDir(session), "actions.jsonl");
41
+ }
42
+
43
+ export function getSessionSnapshotsDir(session: string): string {
44
+ return join(getSessionDir(session), "snapshots");
45
+ }
46
+
47
+ export function getSessionSnapshotRunDir(
48
+ session: string,
49
+ snapshotRunId: string,
50
+ ): string {
51
+ return join(getSessionSnapshotsDir(session), snapshotRunId);
52
+ }
53
+
54
+ export function ensureLibrettoSetup(): void {
55
+ mkdirSync(LIBRETTO_CONFIG_DIR, { recursive: true });
56
+ mkdirSync(LIBRETTO_SESSIONS_DIR, { recursive: true });
57
+ mkdirSync(PROFILES_DIR, { recursive: true });
58
+
59
+ if (!existsSync(LIBRETTO_GITIGNORE_PATH)) {
60
+ writeFileSync(LIBRETTO_GITIGNORE_PATH, LIBRETTO_GITIGNORE_CONTENT, "utf-8");
61
+ }
62
+ }
63
+
64
+ export function createLoggerForSession(session: string): Logger {
65
+ validateSessionName(session);
66
+ const sessionDir = getSessionDir(session);
67
+ mkdirSync(sessionDir, { recursive: true });
68
+ const logFilePath = getSessionLogsPath(session);
69
+ return new Logger(["libretto"], [createFileLogSink({ filePath: logFilePath })]);
70
+ }
71
+
72
+ export async function closeLogger(logger: Logger | null | undefined): Promise<void> {
73
+ if (!logger) return;
74
+ await logger.close();
75
+ }
76
+
77
+ export async function withSessionLogger<T>(
78
+ session: string,
79
+ run: (logger: Logger) => Promise<T>,
80
+ ): Promise<T> {
81
+ const logger = createLoggerForSession(session);
82
+ try {
83
+ return await run(logger);
84
+ } finally {
85
+ await closeLogger(logger);
86
+ }
87
+ }
88
+
89
+ let llmClientFactory:
90
+ | ((logger: LoggerApi, model: string) => Promise<LLMClient>)
91
+ | null = null;
92
+
93
+ export function setLLMClientFactory(
94
+ factory: (logger: LoggerApi, model: string) => Promise<LLMClient>,
95
+ ): void {
96
+ llmClientFactory = factory;
97
+ }
98
+
99
+ export function getLLMClientFactory():
100
+ | ((logger: LoggerApi, model: string) => Promise<LLMClient>)
101
+ | null {
102
+ return llmClientFactory;
103
+ }
104
+
105
+ export function maybeConfigureLLMClientFactoryFromEnv(): void {
106
+ if (llmClientFactory) return;
107
+
108
+ const hasAnyCreds =
109
+ process.env.GOOGLE_CLOUD_PROJECT ||
110
+ process.env.GCLOUD_PROJECT ||
111
+ process.env.ANTHROPIC_API_KEY ||
112
+ process.env.OPENAI_API_KEY ||
113
+ process.env.GEMINI_API_KEY ||
114
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY;
115
+
116
+ if (!hasAnyCreds) return;
117
+
118
+ setLLMClientFactory(async (_logger, model) => {
119
+ const { createLLMClient } = await import("../../shared/llm/index.js");
120
+ return createLLMClient(model);
121
+ });
122
+ }
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ import { unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getSessionDir } from "./context.js";
5
+
6
+ export type PauseSignalPaths = {
7
+ pausedSignalPath: string;
8
+ resumeSignalPath: string;
9
+ completedSignalPath: string;
10
+ failedSignalPath: string;
11
+ outputSignalPath: string;
12
+ };
13
+
14
+ export function getPauseSignalPaths(session: string): PauseSignalPaths {
15
+ const sessionDir = getSessionDir(session);
16
+ return {
17
+ pausedSignalPath: join(sessionDir, `${session}.paused`),
18
+ resumeSignalPath: join(sessionDir, `${session}.resume`),
19
+ completedSignalPath: join(sessionDir, `${session}.completed`),
20
+ failedSignalPath: join(sessionDir, `${session}.failed`),
21
+ outputSignalPath: join(sessionDir, `${session}.output`),
22
+ };
23
+ }
24
+
25
+ export async function removeSignalIfExists(path: string): Promise<void> {
26
+ if (!existsSync(path)) return;
27
+ try {
28
+ await unlink(path);
29
+ } catch (error) {
30
+ const code = (error as NodeJS.ErrnoException).code;
31
+ if (code !== "ENOENT") {
32
+ throw error;
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,553 @@
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 = /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
17
+ const { context, initialPage, logAction, logNetwork } = options;
18
+ const includeUserDomActions = options.includeUserDomActions ?? false;
19
+ const pageIdCache = new WeakMap<Page, string>();
20
+ const wrappedPages = new WeakSet<Page>();
21
+ const exposedPages = new WeakSet<Page>();
22
+
23
+ const resolvePageId = async (page: Page): Promise<string> => {
24
+ if (pageIdCache.has(page)) return pageIdCache.get(page)!;
25
+ const cdpSession = await context.newCDPSession(page);
26
+ try {
27
+ const targetInfo = await cdpSession.send("Target.getTargetInfo");
28
+ const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })?.targetInfo
29
+ ?.targetId;
30
+ if (typeof targetId !== "string" || targetId.length === 0) {
31
+ throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
32
+ }
33
+ pageIdCache.set(page, targetId);
34
+ return targetId;
35
+ } finally {
36
+ await cdpSession.detach();
37
+ }
38
+ };
39
+
40
+ const emitAction = (entry: TelemetryEntry): void => {
41
+ logAction({
42
+ ts: new Date().toISOString(),
43
+ ...entry,
44
+ });
45
+ };
46
+
47
+ const emitNetwork = (entry: TelemetryEntry): void => {
48
+ logNetwork({
49
+ ts: new Date().toISOString(),
50
+ ...entry,
51
+ });
52
+ };
53
+
54
+ const markApiActionInProgress = async (page: Page, inProgress: boolean): Promise<void> => {
55
+ await page.evaluate((flag) => {
56
+ (window as any).__btApiActionInProgress = flag;
57
+ }, inProgress);
58
+ };
59
+
60
+ const wrapLocator = (locator: any, page: Page, pageId: string): any => {
61
+ if (locator.__librettoActionLogged) return locator;
62
+ locator.__librettoActionLogged = true;
63
+
64
+ const locatorActionMethods = [
65
+ "click",
66
+ "dblclick",
67
+ "fill",
68
+ "type",
69
+ "press",
70
+ "check",
71
+ "uncheck",
72
+ "selectOption",
73
+ "hover",
74
+ "focus",
75
+ "scrollIntoViewIfNeeded",
76
+ "waitFor",
77
+ "innerHTML",
78
+ "innerText",
79
+ "textContent",
80
+ "inputValue",
81
+ "isChecked",
82
+ "isDisabled",
83
+ "isEditable",
84
+ "isEnabled",
85
+ "isHidden",
86
+ "isVisible",
87
+ "count",
88
+ "boundingBox",
89
+ "screenshot",
90
+ "evaluate",
91
+ "evaluateAll",
92
+ "evaluateHandle",
93
+ "getAttribute",
94
+ "dispatchEvent",
95
+ "setInputFiles",
96
+ "selectText",
97
+ "dragTo",
98
+ "highlight",
99
+ "tap",
100
+ ] as const;
101
+ const locatorReturningMethods = [
102
+ "first",
103
+ "last",
104
+ "locator",
105
+ "getByRole",
106
+ "getByText",
107
+ "getByLabel",
108
+ "getByPlaceholder",
109
+ "getByAltText",
110
+ "getByTitle",
111
+ "getByTestId",
112
+ "filter",
113
+ "and",
114
+ "or",
115
+ ] as const;
116
+
117
+ for (const actMethod of locatorActionMethods) {
118
+ if (typeof locator[actMethod] !== "function") continue;
119
+ const originalAction = locator[actMethod].bind(locator);
120
+ locator[actMethod] = async (...actionArgs: any[]) => {
121
+ const start = Date.now();
122
+ await markApiActionInProgress(page, true);
123
+ try {
124
+ const result = await originalAction(...actionArgs);
125
+ emitAction({
126
+ pageId,
127
+ action: actMethod,
128
+ source: "agent",
129
+ selector: "locator",
130
+ value:
131
+ actionArgs[0] !== undefined
132
+ ? String(actionArgs[0]).slice(0, 100)
133
+ : undefined,
134
+ duration: Date.now() - start,
135
+ success: true,
136
+ });
137
+ return result;
138
+ } catch (error: any) {
139
+ emitAction({
140
+ pageId,
141
+ action: actMethod,
142
+ source: "agent",
143
+ selector: "locator",
144
+ duration: Date.now() - start,
145
+ success: false,
146
+ error: error?.message ?? String(error),
147
+ });
148
+ throw error;
149
+ } finally {
150
+ await markApiActionInProgress(page, false);
151
+ }
152
+ };
153
+ }
154
+
155
+ for (const method of locatorReturningMethods) {
156
+ if (typeof locator[method] !== "function") continue;
157
+ const originalMethod = locator[method].bind(locator);
158
+ locator[method] = (...args: any[]) => {
159
+ const child = originalMethod(...args);
160
+ return wrapLocator(child, page, pageId);
161
+ };
162
+ }
163
+
164
+ if (typeof locator.nth === "function") {
165
+ const originalNth = locator.nth.bind(locator);
166
+ locator.nth = (index: number) => {
167
+ const child = originalNth(index);
168
+ return wrapLocator(child, page, pageId);
169
+ };
170
+ }
171
+
172
+ if (typeof locator.all === "function") {
173
+ const originalAll = locator.all.bind(locator);
174
+ locator.all = async () => {
175
+ const items: any[] = await originalAll();
176
+ return items.map((item: any) => wrapLocator(item, page, pageId));
177
+ };
178
+ }
179
+
180
+ return locator;
181
+ };
182
+
183
+ const installUserDomTracking = async (page: Page, pageId: string): Promise<void> => {
184
+ if (exposedPages.has(page)) return;
185
+ exposedPages.add(page);
186
+
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
+ });
194
+ });
195
+
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
+ };
219
+
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
+ );
257
+
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,
273
+ );
274
+
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,
289
+ }),
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),
308
+ );
309
+ },
310
+ true,
311
+ );
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,
363
+ );
364
+
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
+ );
385
+ });
386
+ };
387
+
388
+ const wrapPageActions = (page: Page, pageId: string): void => {
389
+ if (wrappedPages.has(page)) return;
390
+ wrappedPages.add(page);
391
+
392
+ const pageActions = [
393
+ "click",
394
+ "dblclick",
395
+ "fill",
396
+ "type",
397
+ "press",
398
+ "check",
399
+ "uncheck",
400
+ "selectOption",
401
+ "hover",
402
+ "focus",
403
+ ] as const;
404
+ const navActions = ["goto", "reload", "goBack", "goForward"] as const;
405
+ const locatorFactories = [
406
+ "locator",
407
+ "getByRole",
408
+ "getByText",
409
+ "getByLabel",
410
+ "getByPlaceholder",
411
+ "getByAltText",
412
+ "getByTitle",
413
+ "getByTestId",
414
+ ] as const;
415
+
416
+ for (const method of pageActions) {
417
+ const originalMethod = (page as any)[method].bind(page);
418
+ (page as any)[method] = async (...args: any[]) => {
419
+ const start = Date.now();
420
+ await markApiActionInProgress(page, true);
421
+ try {
422
+ const result = await originalMethod(...args);
423
+ emitAction({
424
+ pageId,
425
+ action: method,
426
+ source: "agent",
427
+ selector: typeof args[0] === "string" ? args[0] : undefined,
428
+ value: args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
429
+ duration: Date.now() - start,
430
+ success: true,
431
+ });
432
+ return result;
433
+ } catch (error: any) {
434
+ emitAction({
435
+ pageId,
436
+ action: method,
437
+ source: "agent",
438
+ selector: typeof args[0] === "string" ? args[0] : undefined,
439
+ duration: Date.now() - start,
440
+ success: false,
441
+ error: error?.message ?? String(error),
442
+ });
443
+ throw error;
444
+ } finally {
445
+ await markApiActionInProgress(page, false);
446
+ }
447
+ };
448
+ }
449
+
450
+ for (const method of navActions) {
451
+ const originalMethod = (page as any)[method].bind(page);
452
+ (page as any)[method] = async (...args: any[]) => {
453
+ const start = Date.now();
454
+ try {
455
+ const result = await originalMethod(...args);
456
+ emitAction({
457
+ pageId,
458
+ action: method,
459
+ source: "agent",
460
+ url: typeof args[0] === "string" ? args[0] : page.url(),
461
+ duration: Date.now() - start,
462
+ success: true,
463
+ });
464
+ return result;
465
+ } catch (error: any) {
466
+ emitAction({
467
+ pageId,
468
+ action: method,
469
+ source: "agent",
470
+ url: typeof args[0] === "string" ? args[0] : undefined,
471
+ duration: Date.now() - start,
472
+ success: false,
473
+ error: error?.message ?? String(error),
474
+ });
475
+ throw error;
476
+ }
477
+ };
478
+ }
479
+
480
+ for (const factory of locatorFactories) {
481
+ const originalFactory = (page as any)[factory].bind(page);
482
+ (page as any)[factory] = (...factoryArgs: any[]) => {
483
+ const locator = originalFactory(...factoryArgs);
484
+ return wrapLocator(locator, page, pageId);
485
+ };
486
+ }
487
+ };
488
+
489
+ const installForPage = async (page: Page): Promise<void> => {
490
+ const pageId = await resolvePageId(page);
491
+ wrapPageActions(page, pageId);
492
+
493
+ if (includeUserDomActions) {
494
+ await installUserDomTracking(page, pageId);
495
+ }
496
+
497
+ page.on("response", async (response) => {
498
+ const request = response.request();
499
+ const url = request.url();
500
+ if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://")) return;
501
+ emitNetwork({
502
+ pageId,
503
+ method: request.method(),
504
+ url,
505
+ status: response.status(),
506
+ contentType: response.headers()["content-type"] ?? null,
507
+ postData:
508
+ request.method() === "POST" ||
509
+ request.method() === "PUT" ||
510
+ request.method() === "PATCH"
511
+ ? (request.postData() ?? "").substring(0, 2000)
512
+ : undefined,
513
+ responseBody: null,
514
+ size: null,
515
+ durationMs: null,
516
+ });
517
+ });
518
+
519
+ page.on("framenavigated", (frame) => {
520
+ if (frame !== page.mainFrame()) return;
521
+ emitAction({
522
+ pageId,
523
+ action: "navigate",
524
+ source: "agent",
525
+ url: frame.url(),
526
+ success: true,
527
+ });
528
+ });
529
+ page.on("popup", (popup) => {
530
+ emitAction({
531
+ pageId,
532
+ action: "popup",
533
+ source: "agent",
534
+ url: popup.url(),
535
+ success: true,
536
+ });
537
+ });
538
+ page.on("dialog", (dialog) => {
539
+ emitAction({
540
+ pageId,
541
+ action: "dialog",
542
+ source: "agent",
543
+ value: `${dialog.type()}: ${dialog.message().slice(0, 500)}`,
544
+ success: true,
545
+ });
546
+ });
547
+ };
548
+
549
+ await installForPage(initialPage);
550
+ context.on("page", (newPage) => {
551
+ void installForPage(newPage);
552
+ });
553
+ }