opencode-pair-autonomy 1.0.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 (128) hide show
  1. package/README.md +90 -0
  2. package/bin/opencode-pair-autonomy.js +20 -0
  3. package/dist/__tests__/comment-guard.test.d.ts +1 -0
  4. package/dist/__tests__/config.test.d.ts +1 -0
  5. package/dist/__tests__/learning.test.d.ts +1 -0
  6. package/dist/__tests__/plan-mode.test.d.ts +1 -0
  7. package/dist/agents.d.ts +2 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +15351 -0
  10. package/dist/commands.d.ts +2 -0
  11. package/dist/config.d.ts +3 -0
  12. package/dist/hooks/comment-guard.d.ts +15 -0
  13. package/dist/hooks/file-edited.d.ts +7 -0
  14. package/dist/hooks/index.d.ts +46 -0
  15. package/dist/hooks/post-tool-use.d.ts +5 -0
  16. package/dist/hooks/pre-compact.d.ts +4 -0
  17. package/dist/hooks/pre-tool-use.d.ts +5 -0
  18. package/dist/hooks/prompt-refiner.d.ts +38 -0
  19. package/dist/hooks/runtime.d.ts +91 -0
  20. package/dist/hooks/sdk.d.ts +6 -0
  21. package/dist/hooks/session-end.d.ts +4 -0
  22. package/dist/hooks/session-start.d.ts +19 -0
  23. package/dist/hooks/stop.d.ts +5 -0
  24. package/dist/i18n/index.d.ts +15 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +17823 -0
  27. package/dist/installer.d.ts +12 -0
  28. package/dist/learning/analyzer.d.ts +15 -0
  29. package/dist/learning/store.d.ts +4 -0
  30. package/dist/learning/types.d.ts +32 -0
  31. package/dist/mcp.d.ts +4 -0
  32. package/dist/project-facts.d.ts +8 -0
  33. package/dist/prompts/coordinator.d.ts +2 -0
  34. package/dist/prompts/shared.d.ts +5 -0
  35. package/dist/prompts/workers.d.ts +8 -0
  36. package/dist/types.d.ts +81 -0
  37. package/dist/utils.d.ts +6 -0
  38. package/examples/opencode-pair-autonomy.jsonc +35 -0
  39. package/examples/opencode.jsonc +17 -0
  40. package/package.json +103 -0
  41. package/vendor/mcp/pg-mcp/README.md +91 -0
  42. package/vendor/mcp/pg-mcp/config.example.json +26 -0
  43. package/vendor/mcp/pg-mcp/config.json +15 -0
  44. package/vendor/mcp/pg-mcp/package-lock.json +1288 -0
  45. package/vendor/mcp/pg-mcp/package.json +18 -0
  46. package/vendor/mcp/pg-mcp/src/config.js +71 -0
  47. package/vendor/mcp/pg-mcp/src/db.js +85 -0
  48. package/vendor/mcp/pg-mcp/src/index.js +203 -0
  49. package/vendor/mcp/pg-mcp/src/sqlGuard.js +75 -0
  50. package/vendor/mcp/pg-mcp/src/tools.js +89 -0
  51. package/vendor/mcp/ssh-mcp/README.md +46 -0
  52. package/vendor/mcp/ssh-mcp/config.example.json +23 -0
  53. package/vendor/mcp/ssh-mcp/config.json +6 -0
  54. package/vendor/mcp/ssh-mcp/package-lock.json +1142 -0
  55. package/vendor/mcp/ssh-mcp/package.json +18 -0
  56. package/vendor/mcp/ssh-mcp/src/config.js +140 -0
  57. package/vendor/mcp/ssh-mcp/src/index.js +130 -0
  58. package/vendor/mcp/ssh-mcp/src/ssh.js +163 -0
  59. package/vendor/mcp/sudo-mcp/README.md +51 -0
  60. package/vendor/mcp/sudo-mcp/config.example.json +28 -0
  61. package/vendor/mcp/sudo-mcp/config.json +28 -0
  62. package/vendor/mcp/sudo-mcp/package-lock.json +1145 -0
  63. package/vendor/mcp/sudo-mcp/package.json +18 -0
  64. package/vendor/mcp/sudo-mcp/src/config.js +57 -0
  65. package/vendor/mcp/sudo-mcp/src/index.js +267 -0
  66. package/vendor/mcp/sudo-mcp/src/runner.js +168 -0
  67. package/vendor/mcp/web-agent-mcp/package-lock.json +2886 -0
  68. package/vendor/mcp/web-agent-mcp/package.json +28 -0
  69. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/adapter.ts +335 -0
  70. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/auth-heuristics.ts +324 -0
  71. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/launcher.ts +1340 -0
  72. package/vendor/mcp/web-agent-mcp/src/config/env.ts +107 -0
  73. package/vendor/mcp/web-agent-mcp/src/core/action-flow.ts +82 -0
  74. package/vendor/mcp/web-agent-mcp/src/core/artifact-store.ts +109 -0
  75. package/vendor/mcp/web-agent-mcp/src/core/errors.ts +108 -0
  76. package/vendor/mcp/web-agent-mcp/src/core/observation-flow.ts +38 -0
  77. package/vendor/mcp/web-agent-mcp/src/core/policy-engine.ts +113 -0
  78. package/vendor/mcp/web-agent-mcp/src/core/retry-policy.ts +42 -0
  79. package/vendor/mcp/web-agent-mcp/src/core/session-manager.ts +670 -0
  80. package/vendor/mcp/web-agent-mcp/src/core/session-restart-policy.ts +34 -0
  81. package/vendor/mcp/web-agent-mcp/src/core/task-history.ts +97 -0
  82. package/vendor/mcp/web-agent-mcp/src/index.ts +3 -0
  83. package/vendor/mcp/web-agent-mcp/src/schemas/act.ts +167 -0
  84. package/vendor/mcp/web-agent-mcp/src/schemas/common.ts +56 -0
  85. package/vendor/mcp/web-agent-mcp/src/schemas/observe.ts +214 -0
  86. package/vendor/mcp/web-agent-mcp/src/schemas/page.ts +21 -0
  87. package/vendor/mcp/web-agent-mcp/src/schemas/policy.ts +42 -0
  88. package/vendor/mcp/web-agent-mcp/src/schemas/runtime.ts +21 -0
  89. package/vendor/mcp/web-agent-mcp/src/schemas/session.ts +63 -0
  90. package/vendor/mcp/web-agent-mcp/src/server.ts +75 -0
  91. package/vendor/mcp/web-agent-mcp/src/tools/act/click.ts +68 -0
  92. package/vendor/mcp/web-agent-mcp/src/tools/act/drag.ts +57 -0
  93. package/vendor/mcp/web-agent-mcp/src/tools/act/enter-code.ts +78 -0
  94. package/vendor/mcp/web-agent-mcp/src/tools/act/fill.ts +65 -0
  95. package/vendor/mcp/web-agent-mcp/src/tools/act/pinch.ts +58 -0
  96. package/vendor/mcp/web-agent-mcp/src/tools/act/press.ts +67 -0
  97. package/vendor/mcp/web-agent-mcp/src/tools/act/shared.ts +73 -0
  98. package/vendor/mcp/web-agent-mcp/src/tools/act/swipe.ts +59 -0
  99. package/vendor/mcp/web-agent-mcp/src/tools/act/wait-for.ts +56 -0
  100. package/vendor/mcp/web-agent-mcp/src/tools/act/wheel.ts +59 -0
  101. package/vendor/mcp/web-agent-mcp/src/tools/observe/a11y.ts +60 -0
  102. package/vendor/mcp/web-agent-mcp/src/tools/observe/auth-state.ts +92 -0
  103. package/vendor/mcp/web-agent-mcp/src/tools/observe/boxes.ts +66 -0
  104. package/vendor/mcp/web-agent-mcp/src/tools/observe/console.ts +67 -0
  105. package/vendor/mcp/web-agent-mcp/src/tools/observe/dom.ts +60 -0
  106. package/vendor/mcp/web-agent-mcp/src/tools/observe/network.ts +67 -0
  107. package/vendor/mcp/web-agent-mcp/src/tools/observe/page-state.ts +93 -0
  108. package/vendor/mcp/web-agent-mcp/src/tools/observe/screenshot.ts +73 -0
  109. package/vendor/mcp/web-agent-mcp/src/tools/observe/text.ts +70 -0
  110. package/vendor/mcp/web-agent-mcp/src/tools/observe/wait-for-network.ts +70 -0
  111. package/vendor/mcp/web-agent-mcp/src/tools/page/navigate.ts +59 -0
  112. package/vendor/mcp/web-agent-mcp/src/tools/policy/recommend-observation.ts +40 -0
  113. package/vendor/mcp/web-agent-mcp/src/tools/register-tools.ts +55 -0
  114. package/vendor/mcp/web-agent-mcp/src/tools/runtime/evaluate-js.ts +83 -0
  115. package/vendor/mcp/web-agent-mcp/src/tools/session/close.ts +41 -0
  116. package/vendor/mcp/web-agent-mcp/src/tools/session/create.ts +86 -0
  117. package/vendor/mcp/web-agent-mcp/src/tools/session/restart.ts +72 -0
  118. package/vendor/mcp/web-agent-mcp/src/utils/fs.ts +28 -0
  119. package/vendor/mcp/web-agent-mcp/src/utils/ids.ts +9 -0
  120. package/vendor/mcp/web-agent-mcp/src/utils/time.ts +7 -0
  121. package/vendor/mcp/web-agent-mcp/tsconfig.json +22 -0
  122. package/vendor/skills/editorial-technical-ui/SKILL.md +84 -0
  123. package/vendor/skills/figma-console/SKILL.md +839 -0
  124. package/vendor/skills/go-fiber-postgres/SKILL.md +31 -0
  125. package/vendor/skills/opencode-plugin-dev/SKILL.md +31 -0
  126. package/vendor/skills/rust-media-desktop/SKILL.md +30 -0
  127. package/vendor/skills/vue-vite-ui/SKILL.md +31 -0
  128. package/vendor/skills/web-agent-browser/SKILL.md +140 -0
@@ -0,0 +1,1340 @@
1
+ import path from "node:path";
2
+ import type { BrowserContext, Frame, Locator, Page } from "playwright-core";
3
+ import { launchContext, launchPersistentContext } from "cloakbrowser";
4
+ import type { WebAgentEnv } from "../../config/env.js";
5
+ import { createId } from "../../utils/ids.js";
6
+ import { elapsedMs, nowIso } from "../../utils/time.js";
7
+ import type {
8
+ AdapterA11yResult,
9
+ AdapterAuthStateResult,
10
+ AdapterConsoleEntry,
11
+ AdapterDomResult,
12
+ AdapterElementBox,
13
+ AdapterEvaluateResult,
14
+ AdapterNetworkEntry,
15
+ AdapterNavigationResult,
16
+ AdapterPageStateResult,
17
+ AdapterScreenshotResult,
18
+ AdapterSessionCreateInput,
19
+ AdapterSessionHandle,
20
+ AdapterTextResult,
21
+ AdapterWaitForNetworkResult,
22
+ CloakBrowserAdapter,
23
+ WaitUntilState,
24
+ } from "./adapter.js";
25
+ import {
26
+ classifyAuthStateSnapshot,
27
+ normalizeAuthText,
28
+ type AuthFrameInspection,
29
+ } from "./auth-heuristics.js";
30
+
31
+ function truncateText(text: string, maxChars = 12000) {
32
+ if (text.length <= maxChars) {
33
+ return { content: text, truncated: false };
34
+ }
35
+
36
+ return {
37
+ content: text.slice(0, maxChars),
38
+ truncated: true,
39
+ };
40
+ }
41
+
42
+ function pushLimited<T>(items: T[], value: T, maxSize = 200) {
43
+ items.push(value);
44
+ if (items.length > maxSize) {
45
+ items.splice(0, items.length - maxSize);
46
+ }
47
+ }
48
+
49
+ function normalizeWhitespace(text: string) {
50
+ return normalizeAuthText(text);
51
+ }
52
+
53
+ function attachEventBuffers(
54
+ page: AdapterSessionHandle["page"],
55
+ consoleEntries: AdapterConsoleEntry[],
56
+ networkEntries: AdapterNetworkEntry[],
57
+ ) {
58
+ page.on("console", (message) => {
59
+ pushLimited(consoleEntries, {
60
+ type: message.type(),
61
+ text: message.text(),
62
+ location: message.location(),
63
+ timestamp: nowIso(),
64
+ });
65
+ });
66
+
67
+ page.on("response", (response) => {
68
+ const request = response.request();
69
+ pushLimited(networkEntries, {
70
+ url: response.url(),
71
+ method: request.method(),
72
+ status: response.status(),
73
+ resourceType: request.resourceType(),
74
+ outcome: "response",
75
+ timestamp: nowIso(),
76
+ });
77
+ });
78
+
79
+ page.on("requestfailed", (request) => {
80
+ pushLimited(networkEntries, {
81
+ url: request.url(),
82
+ method: request.method(),
83
+ resourceType: request.resourceType(),
84
+ outcome: "failed",
85
+ failureText: request.failure()?.errorText,
86
+ timestamp: nowIso(),
87
+ });
88
+ });
89
+ }
90
+
91
+ async function createPage(context: BrowserContext) {
92
+ const existing = context.pages()[0];
93
+ if (existing) {
94
+ return existing;
95
+ }
96
+ return context.newPage();
97
+ }
98
+
99
+ type InspectableTarget = Page | Frame;
100
+
101
+ type DocumentInspection = {
102
+ title?: string;
103
+ text: string;
104
+ truncated: boolean;
105
+ dom: AdapterDomResult["summary"];
106
+ inputs: AdapterPageStateResult["inputs"];
107
+ buttons: AdapterPageStateResult["buttons"];
108
+ };
109
+
110
+ async function inspectDocument(
111
+ target: InspectableTarget,
112
+ ): Promise<DocumentInspection> {
113
+ const snapshot = await target.evaluate(() => {
114
+ const normalize = (value: string | null | undefined) =>
115
+ value?.replace(/\s+/g, " ").trim() || undefined;
116
+ const isVisible = (element: Element) => {
117
+ const html = element as HTMLElement;
118
+ return Boolean(
119
+ html.offsetWidth || html.offsetHeight || html.getClientRects().length,
120
+ );
121
+ };
122
+ const summarizeElement = (element: Element) => ({
123
+ tag: element.tagName.toLowerCase(),
124
+ type: (element as HTMLInputElement).type || undefined,
125
+ id: element.id || undefined,
126
+ name: element.getAttribute("name") || undefined,
127
+ placeholder: element.getAttribute("placeholder") || undefined,
128
+ text: normalize(element.textContent),
129
+ autocomplete: element.getAttribute("autocomplete") || undefined,
130
+ visible: isVisible(element),
131
+ });
132
+
133
+ const text = (document.body?.innerText ?? "")
134
+ .replace(/\n{3,}/g, "\n\n")
135
+ .trim();
136
+ const headings = Array.from(document.querySelectorAll("h1, h2, h3"))
137
+ .map((node) => normalize(node.textContent))
138
+ .filter((value): value is string => Boolean(value))
139
+ .slice(0, 20);
140
+
141
+ return {
142
+ title: document.title || undefined,
143
+ text,
144
+ dom: {
145
+ headings,
146
+ links: document.querySelectorAll("a[href]").length,
147
+ buttons: document.querySelectorAll("button").length,
148
+ forms: document.querySelectorAll("form").length,
149
+ inputs: document.querySelectorAll("input, textarea, select").length,
150
+ },
151
+ inputs: Array.from(document.querySelectorAll("input, textarea, select"))
152
+ .slice(0, 20)
153
+ .map(summarizeElement),
154
+ buttons: Array.from(document.querySelectorAll("button, [role='button']"))
155
+ .slice(0, 20)
156
+ .map(summarizeElement),
157
+ };
158
+ });
159
+
160
+ const truncated = truncateText(snapshot.text, 4000);
161
+
162
+ return {
163
+ title: snapshot.title,
164
+ text: truncated.content,
165
+ truncated: truncated.truncated,
166
+ dom: snapshot.dom,
167
+ inputs: snapshot.inputs,
168
+ buttons: snapshot.buttons,
169
+ };
170
+ }
171
+
172
+ async function inspectFrames(page: Page) {
173
+ const frames = page.frames().filter((frame) => frame !== page.mainFrame());
174
+ const summaries = await Promise.all(
175
+ frames.map(async (frame: Frame, index) => {
176
+ try {
177
+ const snapshot = await inspectDocument(frame);
178
+ return {
179
+ index,
180
+ name: frame.name() || undefined,
181
+ url: frame.url(),
182
+ title: snapshot.title,
183
+ text_preview: snapshot.text,
184
+ truncated: snapshot.truncated,
185
+ input_count: snapshot.inputs.length,
186
+ button_count: snapshot.buttons.length,
187
+ };
188
+ } catch {
189
+ return {
190
+ index,
191
+ name: frame.name() || undefined,
192
+ url: frame.url(),
193
+ title: undefined,
194
+ text_preview: "",
195
+ truncated: false,
196
+ input_count: 0,
197
+ button_count: 0,
198
+ };
199
+ }
200
+ }),
201
+ );
202
+
203
+ return summaries;
204
+ }
205
+
206
+ async function inspectAuthFrames(page: Page): Promise<AuthFrameInspection[]> {
207
+ const frames = page.frames().filter((frame) => frame !== page.mainFrame());
208
+
209
+ return Promise.all(
210
+ frames.map(async (frame: Frame, index) => {
211
+ try {
212
+ const snapshot = await inspectDocument(frame);
213
+ return {
214
+ index,
215
+ name: frame.name() || undefined,
216
+ url: frame.url(),
217
+ title: snapshot.title,
218
+ text: snapshot.text,
219
+ inputs: snapshot.inputs,
220
+ buttons: snapshot.buttons,
221
+ };
222
+ } catch {
223
+ return {
224
+ index,
225
+ name: frame.name() || undefined,
226
+ url: frame.url(),
227
+ title: undefined,
228
+ text: "",
229
+ inputs: [],
230
+ buttons: [],
231
+ };
232
+ }
233
+ }),
234
+ );
235
+ }
236
+
237
+ function matchNetworkEntry(
238
+ entry: AdapterNetworkEntry,
239
+ input: {
240
+ urlPattern: string;
241
+ useRegex: boolean;
242
+ status?: number;
243
+ outcome?: AdapterNetworkEntry["outcome"];
244
+ },
245
+ ) {
246
+ const urlMatches = input.useRegex
247
+ ? new RegExp(input.urlPattern).test(entry.url)
248
+ : entry.url.includes(input.urlPattern);
249
+
250
+ return (
251
+ urlMatches &&
252
+ (input.status === undefined || entry.status === input.status) &&
253
+ (input.outcome === undefined || entry.outcome === input.outcome)
254
+ );
255
+ }
256
+
257
+ function resolveLocator(page: Page, selector: string, frameSelector?: string) {
258
+ if (frameSelector) {
259
+ return page.frameLocator(frameSelector).locator(selector);
260
+ }
261
+ return page.locator(selector);
262
+ }
263
+
264
+ async function getEditableMeta(locator: ReturnType<Page["locator"]>) {
265
+ return locator.first().evaluate((element) => {
266
+ const html = element as HTMLElement;
267
+ const input = element as HTMLInputElement;
268
+ return {
269
+ tag: element.tagName.toLowerCase(),
270
+ isEditable: html.isContentEditable || element.matches("input, textarea"),
271
+ maxLength: typeof input.maxLength === "number" ? input.maxLength : -1,
272
+ type: input.type || undefined,
273
+ };
274
+ });
275
+ }
276
+
277
+ function isFrameScopedSelector(selector: string) {
278
+ return (
279
+ selector.includes("internal:control=enter-frame") ||
280
+ /(^|\s|>)iframe[.#\[:]/i.test(selector)
281
+ );
282
+ }
283
+
284
+ async function readEditableValue(locator: Locator) {
285
+ return locator.first().evaluate((element) => {
286
+ if (
287
+ element instanceof HTMLInputElement ||
288
+ element instanceof HTMLTextAreaElement ||
289
+ element instanceof HTMLSelectElement
290
+ ) {
291
+ return element.value;
292
+ }
293
+
294
+ if (element instanceof HTMLElement && element.isContentEditable) {
295
+ return element.innerText ?? element.textContent ?? "";
296
+ }
297
+
298
+ return undefined;
299
+ });
300
+ }
301
+
302
+ async function setEditableValueWithDomFallback(
303
+ locator: Locator,
304
+ value: string,
305
+ ) {
306
+ await locator.first().evaluate((element, nextValue) => {
307
+ const dispatch = (target: HTMLElement) => {
308
+ target.dispatchEvent(new Event("input", { bubbles: true }));
309
+ target.dispatchEvent(new Event("change", { bubbles: true }));
310
+ target.dispatchEvent(new Event("blur", { bubbles: true }));
311
+ };
312
+
313
+ if (element instanceof HTMLInputElement) {
314
+ const setter = Object.getOwnPropertyDescriptor(
315
+ HTMLInputElement.prototype,
316
+ "value",
317
+ )?.set;
318
+ setter?.call(element, nextValue);
319
+ dispatch(element);
320
+ return;
321
+ }
322
+
323
+ if (element instanceof HTMLTextAreaElement) {
324
+ const setter = Object.getOwnPropertyDescriptor(
325
+ HTMLTextAreaElement.prototype,
326
+ "value",
327
+ )?.set;
328
+ setter?.call(element, nextValue);
329
+ dispatch(element);
330
+ return;
331
+ }
332
+
333
+ if (element instanceof HTMLSelectElement) {
334
+ const setter = Object.getOwnPropertyDescriptor(
335
+ HTMLSelectElement.prototype,
336
+ "value",
337
+ )?.set;
338
+ setter?.call(element, nextValue);
339
+ dispatch(element);
340
+ return;
341
+ }
342
+
343
+ if (element instanceof HTMLElement && element.isContentEditable) {
344
+ element.textContent = nextValue;
345
+ dispatch(element);
346
+ }
347
+ }, value);
348
+ }
349
+
350
+ function matchesFilledValue(
351
+ actual: string | undefined,
352
+ expected: string,
353
+ appendedText?: string,
354
+ ) {
355
+ if (actual === expected) {
356
+ return true;
357
+ }
358
+
359
+ if (actual && appendedText && actual.endsWith(appendedText)) {
360
+ return true;
361
+ }
362
+
363
+ return normalizeWhitespace(actual ?? "") === normalizeWhitespace(expected);
364
+ }
365
+
366
+ type ClickStateSnapshot = {
367
+ pageUrl: string;
368
+ connected: boolean;
369
+ tag?: string;
370
+ type?: string;
371
+ role?: string;
372
+ disabled?: boolean;
373
+ checked?: boolean;
374
+ ariaPressed?: string;
375
+ ariaExpanded?: string;
376
+ text?: string;
377
+ value?: string;
378
+ };
379
+
380
+ async function captureClickState(
381
+ page: Page,
382
+ locator: Locator,
383
+ ): Promise<ClickStateSnapshot> {
384
+ const elementState = await locator
385
+ .first()
386
+ .evaluate((element) => {
387
+ const input = element as HTMLInputElement;
388
+ return {
389
+ connected: element.isConnected,
390
+ tag: element.tagName.toLowerCase(),
391
+ type: input.type || undefined,
392
+ role: element.getAttribute("role") || undefined,
393
+ disabled: "disabled" in input ? Boolean(input.disabled) : undefined,
394
+ checked: "checked" in input ? Boolean(input.checked) : undefined,
395
+ ariaPressed: element.getAttribute("aria-pressed") || undefined,
396
+ ariaExpanded: element.getAttribute("aria-expanded") || undefined,
397
+ text: normalizeAuthText(element.textContent ?? undefined) || undefined,
398
+ value:
399
+ element instanceof HTMLInputElement ||
400
+ element instanceof HTMLTextAreaElement ||
401
+ element instanceof HTMLSelectElement
402
+ ? element.value
403
+ : undefined,
404
+ };
405
+ })
406
+ .catch(() => ({ connected: false }));
407
+
408
+ return {
409
+ pageUrl: page.url(),
410
+ ...elementState,
411
+ };
412
+ }
413
+
414
+ function didClickCauseProgress(
415
+ before: ClickStateSnapshot,
416
+ after: ClickStateSnapshot,
417
+ ) {
418
+ return (
419
+ before.pageUrl !== after.pageUrl ||
420
+ before.connected !== after.connected ||
421
+ before.disabled !== after.disabled ||
422
+ before.checked !== after.checked ||
423
+ before.ariaPressed !== after.ariaPressed ||
424
+ before.ariaExpanded !== after.ariaExpanded ||
425
+ before.text !== after.text ||
426
+ before.value !== after.value
427
+ );
428
+ }
429
+
430
+ async function canUseDomClickFallback(locator: Locator) {
431
+ return locator
432
+ .first()
433
+ .evaluate((element) => {
434
+ const tag = element.tagName.toLowerCase();
435
+ const type = (element as HTMLInputElement).type?.toLowerCase();
436
+ const role = element.getAttribute("role")?.toLowerCase();
437
+
438
+ return (
439
+ tag === "button" ||
440
+ tag === "a" ||
441
+ role === "button" ||
442
+ (tag === "input" &&
443
+ ["button", "submit", "checkbox", "radio"].includes(type ?? ""))
444
+ );
445
+ })
446
+ .catch(() => false);
447
+ }
448
+
449
+ async function triggerDomClick(locator: Locator) {
450
+ await locator.first().evaluate((element) => {
451
+ if (element instanceof HTMLElement) {
452
+ element.click();
453
+ }
454
+ });
455
+ }
456
+
457
+ async function resolveCodeTargets(page: Page, selector: string) {
458
+ const direct = page.locator(selector);
459
+ const directCount = await direct.count().catch(() => 0);
460
+
461
+ if (directCount > 1) {
462
+ return direct;
463
+ }
464
+
465
+ if (directCount === 1) {
466
+ const meta = await getEditableMeta(direct);
467
+ if (meta.isEditable) {
468
+ return direct;
469
+ }
470
+ }
471
+
472
+ const nested = page.locator(
473
+ `${selector} input, ${selector} textarea, ${selector} [contenteditable='true']`,
474
+ );
475
+ const nestedCount = await nested.count().catch(() => 0);
476
+ if (nestedCount > 0) {
477
+ return nested;
478
+ }
479
+
480
+ return direct;
481
+ }
482
+
483
+ async function getElementCenter(
484
+ page: AdapterSessionHandle["page"],
485
+ selector: string,
486
+ ) {
487
+ const box = await page.locator(selector).first().boundingBox();
488
+ if (!box) {
489
+ throw new Error(
490
+ `Unable to resolve visible bounding box for selector: ${selector}`,
491
+ );
492
+ }
493
+
494
+ return {
495
+ x: box.x + box.width / 2,
496
+ y: box.y + box.height / 2,
497
+ };
498
+ }
499
+
500
+ class PlaywrightCloakBrowserAdapter implements CloakBrowserAdapter {
501
+ constructor(private readonly env: WebAgentEnv) {}
502
+
503
+ async createSession(
504
+ input: AdapterSessionCreateInput,
505
+ ): Promise<AdapterSessionHandle> {
506
+ if (input.profileMode === "persistent") {
507
+ const context = await launchPersistentContext({
508
+ userDataDir:
509
+ input.userDataDir ?? path.join(this.env.profilesDir, input.sessionId),
510
+ headless: this.env.headless,
511
+ locale: input.locale,
512
+ timezone: input.timezoneId,
513
+ humanize: input.humanize,
514
+ args: input.launchArgs,
515
+ viewport: input.viewport,
516
+ });
517
+ const page = await createPage(context);
518
+ const consoleEntries: AdapterConsoleEntry[] = [];
519
+ const networkEntries: AdapterNetworkEntry[] = [];
520
+ attachEventBuffers(page, consoleEntries, networkEntries);
521
+ return {
522
+ contextId: createId("context"),
523
+ pageId: createId("page"),
524
+ context,
525
+ page,
526
+ profileMode: input.profileMode,
527
+ locale: input.locale,
528
+ viewport: input.viewport,
529
+ consoleEntries,
530
+ networkEntries,
531
+ };
532
+ }
533
+
534
+ const context = await launchContext({
535
+ headless: this.env.headless,
536
+ locale: input.locale,
537
+ timezone: input.timezoneId,
538
+ humanize: input.humanize,
539
+ args: input.launchArgs,
540
+ viewport: input.viewport,
541
+ });
542
+ const page = await context.newPage();
543
+ const consoleEntries: AdapterConsoleEntry[] = [];
544
+ const networkEntries: AdapterNetworkEntry[] = [];
545
+ attachEventBuffers(page, consoleEntries, networkEntries);
546
+ return {
547
+ contextId: createId("context"),
548
+ pageId: createId("page"),
549
+ context,
550
+ page,
551
+ profileMode: input.profileMode,
552
+ locale: input.locale,
553
+ viewport: input.viewport,
554
+ consoleEntries,
555
+ networkEntries,
556
+ };
557
+ }
558
+
559
+ async closeSession(session: AdapterSessionHandle) {
560
+ await session.context.close();
561
+ }
562
+
563
+ async navigate(
564
+ session: AdapterSessionHandle,
565
+ url: string,
566
+ waitUntil: WaitUntilState,
567
+ ): Promise<AdapterNavigationResult> {
568
+ const startedAt = Date.now();
569
+ await session.page.goto(url, { waitUntil });
570
+ return {
571
+ pageId: session.pageId,
572
+ requestedUrl: url,
573
+ finalUrl: session.page.url(),
574
+ title: await session.page.title(),
575
+ elapsedMs: elapsedMs(startedAt),
576
+ };
577
+ }
578
+
579
+ async observeA11y(session: AdapterSessionHandle): Promise<AdapterA11yResult> {
580
+ const tree = await session.page.evaluate(() => {
581
+ const selector =
582
+ "a, button, input, textarea, select, [role], [aria-label], [aria-labelledby], h1, h2, h3";
583
+ const children = Array.from(document.querySelectorAll(selector))
584
+ .slice(0, 200)
585
+ .map((element) => ({
586
+ role: element.getAttribute("role") ?? element.tagName.toLowerCase(),
587
+ name:
588
+ element.getAttribute("aria-label") ??
589
+ element.textContent?.replace(/\s+/g, " ").trim() ??
590
+ undefined,
591
+ tag: element.tagName.toLowerCase(),
592
+ text: element.textContent?.replace(/\s+/g, " ").trim() ?? undefined,
593
+ }));
594
+
595
+ return {
596
+ role: "document",
597
+ name: document.title || undefined,
598
+ children,
599
+ };
600
+ });
601
+ return {
602
+ url: session.page.url(),
603
+ title: await session.page.title(),
604
+ tree,
605
+ };
606
+ }
607
+
608
+ async observeDom(session: AdapterSessionHandle): Promise<AdapterDomResult> {
609
+ const summary = await session.page.evaluate(() => {
610
+ const headings = Array.from(document.querySelectorAll("h1, h2, h3"))
611
+ .map((node) => node.textContent?.trim())
612
+ .filter((value): value is string => Boolean(value))
613
+ .slice(0, 20);
614
+
615
+ return {
616
+ headings,
617
+ links: document.querySelectorAll("a[href]").length,
618
+ buttons: document.querySelectorAll("button").length,
619
+ forms: document.querySelectorAll("form").length,
620
+ inputs: document.querySelectorAll("input, textarea, select").length,
621
+ };
622
+ });
623
+
624
+ return {
625
+ url: session.page.url(),
626
+ title: await session.page.title(),
627
+ summary,
628
+ };
629
+ }
630
+
631
+ async observeText(
632
+ session: AdapterSessionHandle,
633
+ format: "text" | "markdown",
634
+ ): Promise<AdapterTextResult> {
635
+ const rawText = await session.page.evaluate(
636
+ () => document.body?.innerText ?? "",
637
+ );
638
+ const normalizedText = rawText.replace(/\n{3,}/g, "\n\n").trim();
639
+ const truncated = truncateText(normalizedText);
640
+
641
+ return {
642
+ url: session.page.url(),
643
+ title: await session.page.title(),
644
+ format,
645
+ content: truncated.content,
646
+ truncated: truncated.truncated,
647
+ };
648
+ }
649
+
650
+ async inspectPageState(
651
+ session: AdapterSessionHandle,
652
+ recentNetworkLimit: number,
653
+ ): Promise<AdapterPageStateResult> {
654
+ const mainDocument = await inspectDocument(session.page);
655
+ const frames = await inspectFrames(session.page);
656
+
657
+ return {
658
+ url: session.page.url(),
659
+ title: await session.page.title(),
660
+ text: mainDocument.text,
661
+ truncated: mainDocument.truncated,
662
+ dom: mainDocument.dom,
663
+ inputs: mainDocument.inputs,
664
+ buttons: mainDocument.buttons,
665
+ frames,
666
+ recentNetwork: session.networkEntries.slice(-recentNetworkLimit),
667
+ };
668
+ }
669
+
670
+ async inspectAuthState(
671
+ session: AdapterSessionHandle,
672
+ recentNetworkLimit: number,
673
+ ): Promise<AdapterAuthStateResult> {
674
+ const mainDocument = await inspectDocument(session.page);
675
+ const frameInspections = await inspectAuthFrames(session.page);
676
+ const frames = frameInspections.map((frame) => ({
677
+ index: frame.index,
678
+ name: frame.name,
679
+ url: frame.url,
680
+ title: frame.title,
681
+ text_preview: frame.text,
682
+ truncated: false,
683
+ input_count: frame.inputs.length,
684
+ button_count: frame.buttons.length,
685
+ }));
686
+ const recentNetwork = session.networkEntries.slice(-recentNetworkLimit);
687
+ const classified = classifyAuthStateSnapshot({
688
+ pageUrl: session.page.url(),
689
+ pageTitle: await session.page.title(),
690
+ pageText: mainDocument.text,
691
+ pageInputs: mainDocument.inputs,
692
+ pageButtons: mainDocument.buttons,
693
+ frames: frameInspections,
694
+ recentNetwork,
695
+ });
696
+
697
+ return {
698
+ url: session.page.url(),
699
+ title: await session.page.title(),
700
+ state: classified.state,
701
+ confidence: classified.confidence,
702
+ summary: classified.summary,
703
+ evidence: classified.evidence,
704
+ suggestedSelectors: classified.suggestedSelectors,
705
+ frames,
706
+ recentNetwork,
707
+ };
708
+ }
709
+
710
+ async takeScreenshot(
711
+ session: AdapterSessionHandle,
712
+ mode: "viewport" | "full" | "element",
713
+ format: "png" | "jpeg",
714
+ quality?: number,
715
+ selector?: string,
716
+ ): Promise<AdapterScreenshotResult> {
717
+ const screenshotOptions = {
718
+ type: format,
719
+ quality: format === "png" ? undefined : quality,
720
+ } as const;
721
+
722
+ if (mode === "element") {
723
+ const locator = session.page.locator(selector ?? "").first();
724
+ const box = await locator.boundingBox();
725
+ const bytes = await locator.screenshot(screenshotOptions);
726
+ return {
727
+ url: session.page.url(),
728
+ title: await session.page.title(),
729
+ bytes,
730
+ mimeType: format === "png" ? "image/png" : "image/jpeg",
731
+ width: box?.width,
732
+ height: box?.height,
733
+ };
734
+ }
735
+
736
+ const bytes = await session.page.screenshot({
737
+ fullPage: mode === "full",
738
+ ...screenshotOptions,
739
+ });
740
+ const viewport = session.page.viewportSize() ?? session.viewport;
741
+ return {
742
+ url: session.page.url(),
743
+ title: await session.page.title(),
744
+ bytes,
745
+ mimeType: format === "png" ? "image/png" : "image/jpeg",
746
+ width: viewport.width,
747
+ height: viewport.height,
748
+ };
749
+ }
750
+
751
+ async observeBoxes(
752
+ session: AdapterSessionHandle,
753
+ selectors: string[],
754
+ ): Promise<AdapterElementBox[]> {
755
+ const boxes = await Promise.all(
756
+ selectors.map(async (selector) => {
757
+ const locator = session.page.locator(selector).first();
758
+ const box = await locator.boundingBox();
759
+ return {
760
+ selector,
761
+ x: box?.x ?? 0,
762
+ y: box?.y ?? 0,
763
+ width: box?.width ?? 0,
764
+ height: box?.height ?? 0,
765
+ visible: Boolean(box),
766
+ };
767
+ }),
768
+ );
769
+
770
+ return boxes;
771
+ }
772
+
773
+ async observeConsole(
774
+ session: AdapterSessionHandle,
775
+ limit: number,
776
+ ): Promise<AdapterConsoleEntry[]> {
777
+ return session.consoleEntries.slice(-limit);
778
+ }
779
+
780
+ async observeNetwork(
781
+ session: AdapterSessionHandle,
782
+ limit: number,
783
+ ): Promise<AdapterNetworkEntry[]> {
784
+ return session.networkEntries.slice(-limit);
785
+ }
786
+
787
+ async waitForNetwork(
788
+ session: AdapterSessionHandle,
789
+ input: {
790
+ urlPattern: string;
791
+ useRegex: boolean;
792
+ status?: number;
793
+ outcome?: AdapterNetworkEntry["outcome"];
794
+ timeoutMs: number;
795
+ pollIntervalMs: number;
796
+ },
797
+ ): Promise<AdapterWaitForNetworkResult> {
798
+ const startedAt = Date.now();
799
+ const existingMatch = [...session.networkEntries]
800
+ .reverse()
801
+ .find((entry) => matchNetworkEntry(entry, input));
802
+
803
+ if (existingMatch) {
804
+ return {
805
+ url: session.page.url(),
806
+ title: await session.page.title(),
807
+ entry: existingMatch,
808
+ elapsedMs: elapsedMs(startedAt),
809
+ };
810
+ }
811
+
812
+ while (Date.now() - startedAt <= input.timeoutMs) {
813
+ const match = [...session.networkEntries]
814
+ .reverse()
815
+ .find((entry) => matchNetworkEntry(entry, input));
816
+ if (match) {
817
+ return {
818
+ url: session.page.url(),
819
+ title: await session.page.title(),
820
+ entry: match,
821
+ elapsedMs: elapsedMs(startedAt),
822
+ };
823
+ }
824
+
825
+ await session.page.waitForTimeout(input.pollIntervalMs);
826
+ }
827
+
828
+ throw new Error(
829
+ `Timed out waiting for network entry matching ${input.urlPattern}`,
830
+ );
831
+ }
832
+
833
+ async evaluateJs(
834
+ session: AdapterSessionHandle,
835
+ input: { expression: string; awaitPromise: boolean },
836
+ ): Promise<AdapterEvaluateResult> {
837
+ const value = await session.page.evaluate(
838
+ async ({ expression, awaitPromise }) => {
839
+ const seen = new WeakSet<object>();
840
+
841
+ const normalize = (current: unknown): unknown => {
842
+ if (current === null || current === undefined) {
843
+ return current;
844
+ }
845
+
846
+ const currentType = typeof current;
847
+
848
+ if (
849
+ currentType === "string" ||
850
+ currentType === "number" ||
851
+ currentType === "boolean"
852
+ ) {
853
+ return current;
854
+ }
855
+
856
+ if (currentType === "bigint") {
857
+ return { __type: "bigint", value: String(current) };
858
+ }
859
+
860
+ if (currentType === "function") {
861
+ return { __type: "function" };
862
+ }
863
+
864
+ if (Array.isArray(current)) {
865
+ return current.map((item) => normalize(item));
866
+ }
867
+
868
+ if (current instanceof Date) {
869
+ return { __type: "date", value: current.toISOString() };
870
+ }
871
+
872
+ if (current instanceof Error) {
873
+ return {
874
+ __type: "error",
875
+ name: current.name,
876
+ message: current.message,
877
+ };
878
+ }
879
+
880
+ if (current instanceof Element) {
881
+ return {
882
+ __type: "element",
883
+ tag: current.tagName.toLowerCase(),
884
+ id: current.id || undefined,
885
+ text:
886
+ current.textContent
887
+ ?.replace(/\s+/g, " ")
888
+ .trim()
889
+ .slice(0, 500) || undefined,
890
+ };
891
+ }
892
+
893
+ if (currentType === "object") {
894
+ const objectValue = current as Record<string, unknown>;
895
+ if (seen.has(objectValue)) {
896
+ return { __type: "circular" };
897
+ }
898
+ seen.add(objectValue);
899
+
900
+ const normalizedEntries = Object.entries(objectValue).map(
901
+ ([key, value]) => [key, normalize(value)],
902
+ );
903
+ return Object.fromEntries(normalizedEntries);
904
+ }
905
+
906
+ return { __type: currentType };
907
+ };
908
+
909
+ const executed = (0, eval)(expression);
910
+ const resolved = awaitPromise ? await executed : executed;
911
+ return normalize(resolved);
912
+ },
913
+ {
914
+ expression: input.expression,
915
+ awaitPromise: input.awaitPromise,
916
+ },
917
+ );
918
+
919
+ return {
920
+ url: session.page.url(),
921
+ title: await session.page.title(),
922
+ value,
923
+ };
924
+ }
925
+
926
+ async click(
927
+ session: AdapterSessionHandle,
928
+ input: {
929
+ selector: string;
930
+ frameSelector?: string;
931
+ button: "left" | "right" | "middle";
932
+ clickCount: number;
933
+ timeoutMs?: number;
934
+ },
935
+ ) {
936
+ const locator = resolveLocator(
937
+ session.page,
938
+ input.selector,
939
+ input.frameSelector,
940
+ ).first();
941
+ const before = await captureClickState(session.page, locator);
942
+ let usedDomFallback = false;
943
+
944
+ try {
945
+ await locator.click({
946
+ button: input.button,
947
+ clickCount: input.clickCount,
948
+ timeout: input.timeoutMs,
949
+ });
950
+ } catch (error) {
951
+ if (
952
+ !isFrameScopedSelector(input.selector) ||
953
+ !(await canUseDomClickFallback(locator))
954
+ ) {
955
+ throw error;
956
+ }
957
+
958
+ await triggerDomClick(locator);
959
+ usedDomFallback = true;
960
+ }
961
+
962
+ await session.page.waitForTimeout(200);
963
+ const after = await captureClickState(session.page, locator);
964
+
965
+ if (
966
+ !usedDomFallback &&
967
+ !didClickCauseProgress(before, after) &&
968
+ isFrameScopedSelector(input.selector) &&
969
+ (await canUseDomClickFallback(locator))
970
+ ) {
971
+ await triggerDomClick(locator);
972
+ await session.page.waitForTimeout(200);
973
+ usedDomFallback = true;
974
+ }
975
+
976
+ return {
977
+ url: session.page.url(),
978
+ title: await session.page.title(),
979
+ verificationHint: usedDomFallback
980
+ ? `Clicked selector ${input.selector} with DOM fallback verification`
981
+ : `Clicked selector ${input.selector}`,
982
+ };
983
+ }
984
+
985
+ async fill(
986
+ session: AdapterSessionHandle,
987
+ input: {
988
+ selector: string;
989
+ frameSelector?: string;
990
+ value: string;
991
+ clearFirst: boolean;
992
+ timeoutMs?: number;
993
+ },
994
+ ) {
995
+ const locator = resolveLocator(
996
+ session.page,
997
+ input.selector,
998
+ input.frameSelector,
999
+ ).first();
1000
+ const initialValue = await readEditableValue(locator).catch(
1001
+ () => undefined,
1002
+ );
1003
+ await locator.fill(
1004
+ input.clearFirst ? "" : await locator.inputValue().catch(() => ""),
1005
+ {
1006
+ timeout: input.timeoutMs,
1007
+ },
1008
+ );
1009
+ if (input.clearFirst) {
1010
+ await locator.fill(input.value, { timeout: input.timeoutMs });
1011
+ } else {
1012
+ await locator.pressSequentially(input.value, {
1013
+ timeout: input.timeoutMs,
1014
+ });
1015
+ }
1016
+ const expectedValue = input.clearFirst
1017
+ ? input.value
1018
+ : `${initialValue ?? ""}${input.value}`;
1019
+ let usedDomFallback = false;
1020
+ const currentValue = await readEditableValue(locator).catch(
1021
+ () => undefined,
1022
+ );
1023
+
1024
+ if (
1025
+ !matchesFilledValue(
1026
+ currentValue,
1027
+ expectedValue,
1028
+ input.clearFirst ? undefined : input.value,
1029
+ )
1030
+ ) {
1031
+ await setEditableValueWithDomFallback(locator, expectedValue);
1032
+ const verifiedValue = await readEditableValue(locator).catch(
1033
+ () => undefined,
1034
+ );
1035
+
1036
+ if (
1037
+ !matchesFilledValue(
1038
+ verifiedValue,
1039
+ expectedValue,
1040
+ input.clearFirst ? undefined : input.value,
1041
+ )
1042
+ ) {
1043
+ throw new Error(
1044
+ `Failed to persist value for selector ${input.selector}`,
1045
+ );
1046
+ }
1047
+
1048
+ usedDomFallback = true;
1049
+ }
1050
+
1051
+ return {
1052
+ url: session.page.url(),
1053
+ title: await session.page.title(),
1054
+ verificationHint: usedDomFallback
1055
+ ? `Filled selector ${input.selector} with DOM persistence fallback`
1056
+ : `Filled selector ${input.selector}`,
1057
+ };
1058
+ }
1059
+
1060
+ async enterCode(
1061
+ session: AdapterSessionHandle,
1062
+ input: {
1063
+ code: string;
1064
+ selector?: string;
1065
+ frameSelector?: string;
1066
+ submit: boolean;
1067
+ timeoutMs?: number;
1068
+ },
1069
+ ) {
1070
+ if (!input.selector) {
1071
+ await session.page.keyboard.type(input.code);
1072
+ if (input.submit) {
1073
+ await session.page.keyboard.press("Enter");
1074
+ }
1075
+ return {
1076
+ url: session.page.url(),
1077
+ title: await session.page.title(),
1078
+ verificationHint: `Typed ${input.code.length}-character code with keyboard focus`,
1079
+ };
1080
+ }
1081
+
1082
+ const targets = input.frameSelector
1083
+ ? resolveLocator(session.page, input.selector, input.frameSelector)
1084
+ : await resolveCodeTargets(session.page, input.selector);
1085
+ const count = await targets.count();
1086
+
1087
+ if (count <= 1) {
1088
+ const locator = targets.first();
1089
+ const meta = await getEditableMeta(targets);
1090
+ await locator.click({ timeout: input.timeoutMs });
1091
+ if (meta.tag === "input" || meta.tag === "textarea") {
1092
+ await locator.fill(input.code, { timeout: input.timeoutMs });
1093
+ } else {
1094
+ await locator.pressSequentially(input.code, {
1095
+ timeout: input.timeoutMs,
1096
+ });
1097
+ }
1098
+ if (input.submit) {
1099
+ await locator.press("Enter", { timeout: input.timeoutMs });
1100
+ }
1101
+ return {
1102
+ url: session.page.url(),
1103
+ title: await session.page.title(),
1104
+ verificationHint: `Entered ${input.code.length}-character code into ${input.selector}`,
1105
+ };
1106
+ }
1107
+
1108
+ const visibleIndexes: number[] = [];
1109
+ for (let index = 0; index < count; index += 1) {
1110
+ if (
1111
+ await targets
1112
+ .nth(index)
1113
+ .isVisible()
1114
+ .catch(() => false)
1115
+ ) {
1116
+ visibleIndexes.push(index);
1117
+ }
1118
+ }
1119
+
1120
+ const targetIndexes =
1121
+ visibleIndexes.length > 0
1122
+ ? visibleIndexes
1123
+ : Array.from({ length: count }, (_, index) => index);
1124
+ if (targetIndexes.length < input.code.length) {
1125
+ throw new Error(
1126
+ `Not enough editable targets to enter ${input.code.length} code characters`,
1127
+ );
1128
+ }
1129
+
1130
+ const characters = [...input.code];
1131
+ for (const [charIndex, char] of characters.entries()) {
1132
+ const locator = targets.nth(targetIndexes[charIndex]!);
1133
+ await locator.click({ timeout: input.timeoutMs });
1134
+ await locator.fill(char, { timeout: input.timeoutMs });
1135
+ }
1136
+
1137
+ if (input.submit) {
1138
+ const lastTargetIndex =
1139
+ targetIndexes[
1140
+ Math.min(characters.length - 1, targetIndexes.length - 1)
1141
+ ]!;
1142
+ await targets.nth(lastTargetIndex).press("Enter", {
1143
+ timeout: input.timeoutMs,
1144
+ });
1145
+ }
1146
+
1147
+ return {
1148
+ url: session.page.url(),
1149
+ title: await session.page.title(),
1150
+ verificationHint: `Entered segmented ${input.code.length}-character code into ${input.selector}`,
1151
+ };
1152
+ }
1153
+
1154
+ async press(
1155
+ session: AdapterSessionHandle,
1156
+ input: {
1157
+ key: string;
1158
+ selector?: string;
1159
+ frameSelector?: string;
1160
+ timeoutMs?: number;
1161
+ },
1162
+ ) {
1163
+ if (input.selector) {
1164
+ const locator = resolveLocator(
1165
+ session.page,
1166
+ input.selector,
1167
+ input.frameSelector,
1168
+ ).first();
1169
+ await locator.press(input.key, { timeout: input.timeoutMs });
1170
+ } else {
1171
+ await session.page.keyboard.press(input.key);
1172
+ }
1173
+ return {
1174
+ url: session.page.url(),
1175
+ title: await session.page.title(),
1176
+ verificationHint: input.selector
1177
+ ? `Pressed ${input.key} on ${input.selector}`
1178
+ : `Pressed ${input.key}`,
1179
+ };
1180
+ }
1181
+
1182
+ async waitFor(
1183
+ session: AdapterSessionHandle,
1184
+ input: { selector?: string; text?: string; timeoutMs: number },
1185
+ ) {
1186
+ if (input.selector) {
1187
+ await session.page.waitForSelector(input.selector, {
1188
+ timeout: input.timeoutMs,
1189
+ });
1190
+ } else if (input.text) {
1191
+ await session.page
1192
+ .getByText(input.text, { exact: false })
1193
+ .first()
1194
+ .waitFor({ timeout: input.timeoutMs });
1195
+ }
1196
+ return {
1197
+ url: session.page.url(),
1198
+ title: await session.page.title(),
1199
+ verificationHint: input.selector
1200
+ ? `Observed selector ${input.selector}`
1201
+ : `Observed text ${input.text}`,
1202
+ };
1203
+ }
1204
+
1205
+ async wheel(
1206
+ session: AdapterSessionHandle,
1207
+ input: {
1208
+ selector?: string;
1209
+ deltaX: number;
1210
+ deltaY: number;
1211
+ steps: number;
1212
+ stepDelayMs: number;
1213
+ timeoutMs?: number;
1214
+ },
1215
+ ) {
1216
+ if (input.selector) {
1217
+ await session.page
1218
+ .locator(input.selector)
1219
+ .first()
1220
+ .hover({ timeout: input.timeoutMs });
1221
+ }
1222
+
1223
+ const stepX = input.deltaX / input.steps;
1224
+ const stepY = input.deltaY / input.steps;
1225
+
1226
+ for (let index = 0; index < input.steps; index += 1) {
1227
+ await session.page.mouse.wheel(stepX, stepY);
1228
+ if (input.stepDelayMs > 0 && index < input.steps - 1) {
1229
+ await session.page.waitForTimeout(input.stepDelayMs);
1230
+ }
1231
+ }
1232
+
1233
+ return {
1234
+ url: session.page.url(),
1235
+ title: await session.page.title(),
1236
+ verificationHint: input.selector
1237
+ ? `Scrolled on ${input.selector}`
1238
+ : `Scrolled viewport by (${input.deltaX}, ${input.deltaY})`,
1239
+ };
1240
+ }
1241
+
1242
+ async drag(
1243
+ session: AdapterSessionHandle,
1244
+ input: {
1245
+ fromSelector: string;
1246
+ toSelector: string;
1247
+ steps: number;
1248
+ timeoutMs?: number;
1249
+ },
1250
+ ) {
1251
+ const source = await getElementCenter(session.page, input.fromSelector);
1252
+ const target = await getElementCenter(session.page, input.toSelector);
1253
+ await session.page.mouse.move(source.x, source.y);
1254
+ await session.page.mouse.down({ button: "left" });
1255
+ await session.page.mouse.move(target.x, target.y, { steps: input.steps });
1256
+ await session.page.mouse.up({ button: "left" });
1257
+
1258
+ return {
1259
+ url: session.page.url(),
1260
+ title: await session.page.title(),
1261
+ verificationHint: `Dragged from ${input.fromSelector} to ${input.toSelector}`,
1262
+ };
1263
+ }
1264
+
1265
+ async swipe(
1266
+ session: AdapterSessionHandle,
1267
+ input: {
1268
+ selector?: string;
1269
+ startX?: number;
1270
+ startY?: number;
1271
+ deltaX: number;
1272
+ deltaY: number;
1273
+ speed: number;
1274
+ },
1275
+ ) {
1276
+ const cdp = await session.page.context().newCDPSession(session.page);
1277
+ const start = input.selector
1278
+ ? await getElementCenter(session.page, input.selector)
1279
+ : {
1280
+ x: input.startX ?? 0,
1281
+ y: input.startY ?? 0,
1282
+ };
1283
+
1284
+ await cdp.send("Input.synthesizeScrollGesture", {
1285
+ x: Math.round(start.x),
1286
+ y: Math.round(start.y),
1287
+ xDistance: input.deltaX,
1288
+ yDistance: input.deltaY,
1289
+ speed: input.speed,
1290
+ gestureSourceType: "touch",
1291
+ });
1292
+
1293
+ return {
1294
+ url: session.page.url(),
1295
+ title: await session.page.title(),
1296
+ verificationHint: input.selector
1297
+ ? `Swiped on ${input.selector}`
1298
+ : `Swiped from (${start.x}, ${start.y}) by (${input.deltaX}, ${input.deltaY})`,
1299
+ };
1300
+ }
1301
+
1302
+ async pinch(
1303
+ session: AdapterSessionHandle,
1304
+ input: {
1305
+ selector?: string;
1306
+ centerX?: number;
1307
+ centerY?: number;
1308
+ scaleFactor: number;
1309
+ speed: number;
1310
+ },
1311
+ ) {
1312
+ const cdp = await session.page.context().newCDPSession(session.page);
1313
+ const center = input.selector
1314
+ ? await getElementCenter(session.page, input.selector)
1315
+ : {
1316
+ x: input.centerX ?? 0,
1317
+ y: input.centerY ?? 0,
1318
+ };
1319
+
1320
+ await cdp.send("Input.synthesizePinchGesture", {
1321
+ x: Math.round(center.x),
1322
+ y: Math.round(center.y),
1323
+ scaleFactor: input.scaleFactor,
1324
+ relativeSpeed: input.speed,
1325
+ gestureSourceType: "touch",
1326
+ });
1327
+
1328
+ return {
1329
+ url: session.page.url(),
1330
+ title: await session.page.title(),
1331
+ verificationHint: input.selector
1332
+ ? `Pinched on ${input.selector} with scale ${input.scaleFactor}`
1333
+ : `Pinched at (${center.x}, ${center.y}) with scale ${input.scaleFactor}`,
1334
+ };
1335
+ }
1336
+ }
1337
+
1338
+ export function createCloakBrowserAdapter(env: WebAgentEnv) {
1339
+ return new PlaywrightCloakBrowserAdapter(env);
1340
+ }