aether-mcp-server 2.0.2 → 2.1.1

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.
@@ -1,724 +1,399 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.CdpBridge = void 0;
4
37
  exports.getCdpBridge = getCdpBridge;
38
+ /**
39
+ * CdpBridge — thin command router that delegates to focused bridge modules.
40
+ *
41
+ * Previously this was a 2,761-line God object. Now it's ~350 lines of routing
42
+ * logic. All actual browser operations live in the bridge/ modules.
43
+ */
5
44
  const cdp_client_1 = require("./cdp-client");
6
- const captcha_solver_1 = require("./captcha-solver");
7
45
  const locator_engine_1 = require("./locator-engine");
8
46
  const page_snapshot_cache_1 = require("./page-snapshot-cache");
9
- /**
10
- * Bridge layer that translates old extension-style commands to CDP commands.
11
- * This allows the MCP server to work without the Chrome extension.
12
- */
47
+ const captcha_solver_1 = require("./captcha-solver");
48
+ const logger_1 = require("./logger");
49
+ // Bridge modules
50
+ const Navigation = __importStar(require("./bridge/navigation"));
51
+ const Interaction = __importStar(require("./bridge/interaction"));
52
+ const Inspection = __importStar(require("./bridge/inspection"));
53
+ const Session = __importStar(require("./bridge/session"));
54
+ const Debugging = __importStar(require("./bridge/debugging"));
55
+ // Eval scripts (for methods not yet fully extracted)
56
+ const eval_scripts_1 = require("./eval-scripts");
13
57
  class CdpBridge {
14
58
  client;
15
59
  locator;
16
60
  snapshotCache;
61
+ logger;
62
+ // Screencast state (kept here until extracted to module)
63
+ screencastFrames = [];
64
+ screencastFrameListener = null;
65
+ mockRoutes = [];
66
+ mockRouteListener = null;
17
67
  constructor() {
18
68
  this.client = (0, cdp_client_1.getCdpClient)();
19
69
  this.locator = new locator_engine_1.LocatorEngine(this.client);
20
70
  this.snapshotCache = new page_snapshot_cache_1.PageSnapshotCache(this.client, this.locator);
71
+ this.logger = (0, logger_1.createLogger)("bridge");
21
72
  }
73
+ // ─── Speed Control ────────────────────────────────────────────────
74
+ getSpeedMultiplier() {
75
+ return this.client.getSpeedMultiplier();
76
+ }
77
+ setSpeed(m) {
78
+ this.client.setSpeed(m);
79
+ }
80
+ // ─── Main Command Router ──────────────────────────────────────────
81
+ async sendCommand(method, params = {}) {
82
+ const log = this.logger.child(method);
83
+ try {
84
+ switch (method) {
85
+ // ─── Navigation ────────────────────────────────────────
86
+ case "connect":
87
+ return Navigation.connect(this.client, params);
88
+ case "navigate":
89
+ await this.ensureConnected();
90
+ return Navigation.navigate(this.client, params, this.snapshotCache);
91
+ case "smart_navigate":
92
+ await this.ensureConnected();
93
+ return Navigation.smartNavigate(this.client, params, this.snapshotCache, log);
94
+ case "new_tab":
95
+ await this.ensureConnected();
96
+ return Navigation.newTab(this.client, params);
97
+ case "switch_tab":
98
+ await this.ensureConnected();
99
+ return Navigation.switchTab(this.client, params);
100
+ case "close_tab":
101
+ await this.ensureConnected();
102
+ return Navigation.closeTab(this.client, params);
103
+ case "get_tabs":
104
+ await this.ensureConnected();
105
+ return Navigation.getTabs(this.client, params);
106
+ // ─── Inspection ────────────────────────────────────────
107
+ case "browser_status":
108
+ return Inspection.browserStatus(this.client, params);
109
+ case "snapshot_compact":
110
+ await this.ensureConnected();
111
+ return Inspection.snapshotCompact(this.client, this.snapshotCache, params);
112
+ case "list_interactive_elements":
113
+ await this.ensureConnected();
114
+ return Inspection.listInteractiveElements(this.client, this.snapshotCache, this.locator, params);
115
+ case "get_state":
116
+ await this.ensureConnected();
117
+ return Inspection.getState(this.client, this.snapshotCache, this.locator, params);
118
+ case "page_snapshot":
119
+ await this.ensureConnected();
120
+ return Inspection.pageSnapshot(this.client, params);
121
+ case "get_page_text":
122
+ await this.ensureConnected();
123
+ return Inspection.getPageText(this.client, params);
124
+ case "get_tree":
125
+ await this.ensureConnected();
126
+ return Inspection.getAccessibilityTree(this.client);
127
+ case "get_dom_tree":
128
+ case "get_dom_snapshot":
129
+ await this.ensureConnected();
130
+ return Inspection.getDOMTree(this.client);
131
+ case "highlight_elements":
132
+ await this.ensureConnected();
133
+ return Inspection.highlightElements(this.client);
134
+ case "verify_ui_state":
135
+ await this.ensureConnected();
136
+ return Inspection.verifyUIState(this.client, params);
137
+ case "get_computed_style":
138
+ await this.ensureConnected();
139
+ return Inspection.getComputedStyle(this.client, params);
140
+ case "get_event_listeners":
141
+ await this.ensureConnected();
142
+ return Inspection.getEventListeners(this.client, params);
143
+ // ─── Interaction ───────────────────────────────────────
144
+ case "click_by_ref":
145
+ await this.ensureConnected();
146
+ return Interaction.clickByRef(this.client, this.locator, this.snapshotCache, params, log);
147
+ case "click_by_selector":
148
+ await this.ensureConnected();
149
+ return Interaction.clickBySelector(this.client, this.locator, this.snapshotCache, params, log);
150
+ case "fill_by_selector":
151
+ await this.ensureConnected();
152
+ return Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, params, log);
153
+ case "wait_for_selector":
154
+ await this.ensureConnected();
155
+ return this.waitForSelectorCompact(params);
156
+ case "wait_for_text":
157
+ await this.ensureConnected();
158
+ return this.waitForText(params);
159
+ case "press_key":
160
+ case "key_combo":
161
+ await this.ensureConnected();
162
+ return Interaction.pressKey(this.client, this.locator, this.snapshotCache, params, log);
163
+ case "click_text":
164
+ await this.ensureConnected();
165
+ return Interaction.clickText(this.client, this.locator, this.snapshotCache, params, log);
166
+ case "click_role":
167
+ await this.ensureConnected();
168
+ return Interaction.clickRole(this.client, this.locator, this.snapshotCache, params, log);
169
+ case "fill_label":
170
+ await this.ensureConnected();
171
+ return Interaction.fillLabel(this.client, this.locator, this.snapshotCache, params, log);
172
+ case "click":
173
+ await this.ensureConnected();
174
+ return Interaction.click(this.client, this.locator, this.snapshotCache, params, log);
175
+ case "click_element":
176
+ await this.ensureConnected();
177
+ return Interaction.clickElement(this.client, this.locator, this.snapshotCache, params, log);
178
+ case "click_element_by_selector":
179
+ await this.ensureConnected();
180
+ return Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, params, log);
181
+ case "type":
182
+ await this.ensureConnected();
183
+ return Interaction.type(this.client, this.locator, this.snapshotCache, params, log);
184
+ case "fill":
185
+ await this.ensureConnected();
186
+ return Interaction.fillInput(this.client, this.locator, this.snapshotCache, params, log);
187
+ case "select":
188
+ await this.ensureConnected();
189
+ return Interaction.selectOption(this.client, this.locator, this.snapshotCache, params, log);
190
+ case "check":
191
+ await this.ensureConnected();
192
+ return Interaction.checkElement(this.client, this.locator, this.snapshotCache, params, log);
193
+ case "hover":
194
+ await this.ensureConnected();
195
+ return Interaction.hover(this.client, this.locator, this.snapshotCache, params, log);
196
+ case "drag_and_drop":
197
+ await this.ensureConnected();
198
+ return Interaction.dragAndDrop(this.client, this.locator, this.snapshotCache, params, log);
199
+ case "scroll":
200
+ await this.ensureConnected();
201
+ return Interaction.scroll(this.client, this.locator, this.snapshotCache, params, log);
202
+ case "wait":
203
+ await this.ensureConnected();
204
+ return Interaction.wait(this.client, this.locator, this.snapshotCache, params, log);
205
+ case "element_at_point":
206
+ await this.ensureConnected();
207
+ return Interaction.elementAtPoint(this.client, this.locator, this.snapshotCache, params, log);
208
+ case "browser_intent":
209
+ await this.ensureConnected();
210
+ return this.browserIntent(params);
211
+ case "screenshot":
212
+ case "screenshot_region":
213
+ await this.ensureConnected();
214
+ return this.screenshot(params);
215
+ case "evaluate":
216
+ await this.ensureConnected();
217
+ return Debugging.evaluate(this.client, params);
218
+ // ─── Session ───────────────────────────────────────────
219
+ case "save_auth_state":
220
+ await this.ensureConnected();
221
+ return Session.saveAuthState(this.client, params, this.snapshotCache);
222
+ case "load_auth_state":
223
+ await this.ensureConnected();
224
+ return Session.loadAuthState(this.client, params, this.snapshotCache);
225
+ case "get_cookies":
226
+ await this.ensureConnected();
227
+ return Session.getCookies(this.client);
228
+ case "set_cookie":
229
+ await this.ensureConnected();
230
+ return Session.setCookie(this.client, params);
231
+ case "clear_cache":
232
+ await this.ensureConnected();
233
+ return Session.clearCache(this.client);
234
+ // ─── Debugging ──────────────────────────────────────────
235
+ case "get_logs":
236
+ await this.ensureConnected();
237
+ return Debugging.getLogs(this.client, params);
238
+ case "get_network_errors":
239
+ await this.ensureConnected();
240
+ return Debugging.getNetworkErrors(this.client, params);
241
+ case "get_network_traffic":
242
+ await this.ensureConnected();
243
+ return Debugging.getNetworkTraffic(this.client);
244
+ case "get_network_response":
245
+ await this.ensureConnected();
246
+ return Debugging.getNetworkResponse(this.client, params);
247
+ case "get_performance_metrics":
248
+ await this.ensureConnected();
249
+ return Debugging.getPerformanceMetrics(this.client);
250
+ case "start_tracing":
251
+ await this.ensureConnected();
252
+ return Debugging.startTracing(this.client, params);
253
+ case "stop_tracing":
254
+ await this.ensureConnected();
255
+ return Debugging.stopTracing(this.client);
256
+ case "cdp_command":
257
+ await this.ensureConnected();
258
+ return Debugging.cdpCommand(this.client, params);
259
+ case "assert":
260
+ await this.ensureConnected();
261
+ return Debugging.assertCondition(this.client, params);
262
+ case "get_dom_storage":
263
+ await this.ensureConnected();
264
+ return Debugging.getDOMStorage(this.client, params);
265
+ // ─── CAPTCHA ───────────────────────────────────────────
266
+ case "detect_captcha":
267
+ await this.ensureConnected();
268
+ return this.detectCaptcha();
269
+ case "solve_captcha":
270
+ await this.ensureConnected();
271
+ return this.solveCaptchaAction(params);
272
+ // ─── Configuration ─────────────────────────────────────
273
+ case "configure":
274
+ await this.ensureConnected();
275
+ return this.configureBrowser(params);
276
+ case "emulate_network":
277
+ await this.ensureConnected();
278
+ return this.emulateNetworkConditions(params);
279
+ case "set_geolocation":
280
+ await this.ensureConnected();
281
+ return this.setGeolocation(params);
282
+ case "set_timezone":
283
+ await this.ensureConnected();
284
+ return this.setTimezone(params);
285
+ case "print_pdf":
286
+ await this.ensureConnected();
287
+ return this.printPDF(params);
288
+ case "upload_file":
289
+ await this.ensureConnected();
290
+ return this.uploadFile(params);
291
+ case "mock_network_request":
292
+ await this.ensureConnected();
293
+ return this.mockNetworkRequest(params);
294
+ // ─── Screencast ────────────────────────────────────────
295
+ case "start_screencast":
296
+ await this.ensureConnected();
297
+ return this.startScreencast(params);
298
+ case "stop_screencast":
299
+ await this.ensureConnected();
300
+ return this.stopScreencast(params);
301
+ case "record_session":
302
+ await this.ensureConnected();
303
+ return this.recordSession(params);
304
+ case "sample_visual_frames":
305
+ await this.ensureConnected();
306
+ return this.sampleVisualFrames(params);
307
+ case "get_screencast_frames":
308
+ await this.ensureConnected();
309
+ return this.getScreencastFrames(params);
310
+ // ─── Agent APIs ────────────────────────────────────────
311
+ case "agent_action":
312
+ await this.ensureConnected();
313
+ return this.agentAction(params);
314
+ case "observe_and_act":
315
+ await this.ensureConnected();
316
+ return this.observeAndAct(params);
317
+ case "agent_form_fill":
318
+ await this.ensureConnected();
319
+ return this.agentFormFill(params);
320
+ default:
321
+ await this.ensureConnected();
322
+ return this.client.sendCommand(method, params);
323
+ }
324
+ }
325
+ catch (err) {
326
+ log.error("Command failed", { error: err.message, method });
327
+ throw err;
328
+ }
329
+ }
330
+ // ─── Connection helpers ───────────────────────────────────────────
22
331
  async ensureConnected() {
23
332
  if (!this.client.isConnected()) {
24
- // Try to connect to existing Chrome on default port
25
333
  try {
26
334
  await this.client.connect(9222);
27
335
  }
28
336
  catch {
29
- // Chrome not running, launch it
30
337
  await this.client.launch({ headless: false });
31
338
  }
32
339
  }
33
340
  }
34
- async applyCognitivePause(type) {
35
- let delay = 150 + Math.random() * 250; // base delay (150-400ms)
36
- if (type === "type" || type === "fill") {
37
- delay += 150 + Math.random() * 250; // typing prep delay (300-650ms total)
38
- }
39
- else if (type === "click") {
40
- delay += 50 + Math.random() * 150; // click prep delay (200-550ms total)
41
- }
42
- console.error(`[Bridge] Emulating cognitive pause: ${Math.round(delay)}ms for action type: ${type}`);
43
- await new Promise((r) => setTimeout(r, delay));
44
- }
45
- async sendCommand(method, params = {}) {
46
- // Cognitive pause mapping
47
- const interactionTypes = {
48
- click_by_ref: "click",
49
- click_by_selector: "click",
50
- fill_by_selector: "fill",
51
- press_key: "key",
52
- key_combo: "key",
53
- click_text: "click",
54
- click_role: "click",
55
- fill_label: "fill",
56
- click: "click",
57
- click_element: "click",
58
- click_element_by_selector: "click",
59
- type: "type",
60
- fill: "fill",
61
- select: "click",
62
- check: "click",
63
- hover: "click",
64
- drag_and_drop: "click"
65
- };
66
- if (interactionTypes[method]) {
67
- await this.applyCognitivePause(interactionTypes[method]);
68
- }
69
- switch (method) {
70
- case "browser_status":
71
- return this.browserStatus(params);
72
- case "snapshot_compact":
73
- await this.ensureConnected();
74
- return this.snapshotCompact(params);
75
- case "list_interactive_elements":
76
- await this.ensureConnected();
77
- return this.listInteractiveElements(params);
78
- case "click_by_ref":
79
- await this.ensureConnected();
80
- return this.clickByRef(params);
81
- case "click_by_selector":
82
- await this.ensureConnected();
83
- return this.clickBySelector(params);
84
- case "fill_by_selector":
85
- await this.ensureConnected();
86
- return this.fillBySelector(params);
87
- case "wait_for_selector":
88
- await this.ensureConnected();
89
- return this.waitForSelectorCompact(params);
90
- case "wait_for_text":
91
- await this.ensureConnected();
92
- return this.waitForText(params);
93
- case "get_network_errors":
94
- await this.ensureConnected();
95
- return this.getNetworkErrors(params);
96
- case "detect_captcha":
97
- await this.ensureConnected();
98
- return this.detectCaptcha();
99
- case "solve_captcha":
100
- await this.ensureConnected();
101
- return this.solveCaptchaAction(params);
102
- case "browser_intent":
103
- await this.ensureConnected();
104
- await this.ensureNoCaptcha("browser_intent");
105
- return this.browserIntent(params);
106
- case "get_logs":
107
- await this.ensureConnected();
108
- return this.getLogs(params);
109
- case "press_key":
110
- case "key_combo":
111
- await this.ensureConnected();
112
- await this.ensureNoCaptcha("press_key");
113
- return this.pressKey(params);
114
- case "click_text":
115
- await this.ensureConnected();
116
- await this.ensureNoCaptcha("click_text");
117
- return this.clickText(params);
118
- case "click_role":
119
- await this.ensureConnected();
120
- await this.ensureNoCaptcha("click_role");
121
- return this.clickRole(params);
122
- case "fill_label":
123
- await this.ensureConnected();
124
- await this.ensureNoCaptcha("fill_label");
125
- return this.fillLabel(params);
126
- case "element_at_point":
127
- await this.ensureConnected();
128
- return this.elementAtPoint(params);
129
- case "connect":
130
- return this.connect(params);
131
- case "get_state":
132
- await this.ensureConnected();
133
- return this.getState(params);
134
- case "navigate":
135
- await this.ensureConnected();
136
- return this.navigate(params);
137
- case "click":
138
- await this.ensureConnected();
139
- await this.ensureNoCaptcha("click");
140
- return this.click(params);
141
- case "click_element":
142
- await this.ensureConnected();
143
- await this.ensureNoCaptcha("click_element");
144
- return this.clickElement(params);
145
- case "click_element_by_selector":
146
- await this.ensureConnected();
147
- await this.ensureNoCaptcha("click_element_by_selector");
148
- return this.clickElementBySelector(params);
149
- case "type":
150
- await this.ensureConnected();
151
- await this.ensureNoCaptcha("type");
152
- return this.type(params);
153
- case "evaluate":
154
- await this.ensureConnected();
155
- return this.evaluate(params);
156
- case "screenshot":
157
- case "screenshot_region":
158
- await this.ensureConnected();
159
- return this.screenshot(params);
160
- case "scroll":
161
- await this.ensureConnected();
162
- return this.scroll(params);
163
- case "wait":
164
- await this.ensureConnected();
165
- return this.wait(params);
166
- case "cdp_command":
167
- await this.ensureConnected();
168
- return this.cdpCommand(params);
169
- case "get_dom_snapshot":
170
- await this.ensureConnected();
171
- return this.getDomSnapshot(params);
172
- case "get_tabs":
173
- await this.ensureConnected();
174
- return this.getTabs(params);
175
- case "new_tab":
176
- await this.ensureConnected();
177
- return this.newTab(params);
178
- case "switch_tab":
179
- await this.ensureConnected();
180
- return this.switchTab(params);
181
- case "close_tab":
182
- await this.ensureConnected();
183
- return this.closeTab(params);
184
- case "start_screencast":
185
- await this.ensureConnected();
186
- return this.startScreencast(params);
187
- case "stop_screencast":
188
- await this.ensureConnected();
189
- return this.stopScreencast(params);
190
- case "record_session":
191
- await this.ensureConnected();
192
- return this.recordSession(params);
193
- case "sample_visual_frames":
194
- await this.ensureConnected();
195
- return this.sampleVisualFrames(params);
196
- case "start_tracing":
197
- await this.ensureConnected();
198
- return this.startTracing(params);
199
- case "stop_tracing":
200
- await this.ensureConnected();
201
- return this.stopTracing(params);
202
- case "get_performance_metrics":
203
- await this.ensureConnected();
204
- return this.getPerformanceMetrics(params);
205
- case "hover":
206
- await this.ensureConnected();
207
- await this.ensureNoCaptcha("hover");
208
- return this.hover(params);
209
- case "drag_and_drop":
210
- await this.ensureConnected();
211
- await this.ensureNoCaptcha("drag_and_drop");
212
- return this.dragAndDrop(params);
213
- // ==================== MISSING ACT TOOL ACTIONS ====================
214
- case "fill":
215
- await this.ensureConnected();
216
- await this.ensureNoCaptcha("fill");
217
- return this.fillInput(params);
218
- case "select":
219
- await this.ensureConnected();
220
- await this.ensureNoCaptcha("select");
221
- return this.selectOption(params);
222
- case "check":
223
- await this.ensureConnected();
224
- await this.ensureNoCaptcha("check");
225
- return this.checkElement(params);
226
- case "get_tree":
227
- await this.ensureConnected();
228
- return this.getAccessibilityTree(params);
229
- case "get_dom_tree":
230
- await this.ensureConnected();
231
- return this.getDOMTree(params);
232
- case "assert":
233
- await this.ensureConnected();
234
- return this.assertCondition(params);
235
- case "get_cookies":
236
- await this.ensureConnected();
237
- return this.getCookies(params);
238
- case "set_cookie":
239
- await this.ensureConnected();
240
- return this.setCookie(params);
241
- case "clear_cache":
242
- await this.ensureConnected();
243
- return this.clearCache(params);
244
- case "set_geolocation":
245
- await this.ensureConnected();
246
- return this.setGeolocation(params);
247
- case "set_timezone":
248
- await this.ensureConnected();
249
- return this.setTimezone(params);
250
- case "emulate_network":
251
- await this.ensureConnected();
252
- return this.emulateNetworkConditions(params);
253
- case "print_pdf":
254
- await this.ensureConnected();
255
- return this.printPDF(params);
256
- case "highlight_elements":
257
- await this.ensureConnected();
258
- return this.highlightElements(params);
259
- case "verify_ui_state":
260
- await this.ensureConnected();
261
- return this.verifyUIState(params);
262
- case "get_dom_storage":
263
- await this.ensureConnected();
264
- return this.getDOMStorage(params);
265
- case "get_network_traffic":
266
- await this.ensureConnected();
267
- return this.getNetworkTraffic(params);
268
- case "get_network_response":
269
- await this.ensureConnected();
270
- return this.getNetworkResponse(params);
271
- case "mock_network_request":
272
- await this.ensureConnected();
273
- return this.mockNetworkRequest(params);
274
- case "get_computed_style":
275
- await this.ensureConnected();
276
- return this.getComputedStyle(params);
277
- case "get_event_listeners":
278
- await this.ensureConnected();
279
- return this.getEventListeners(params);
280
- case "get_screencast_frames":
281
- await this.ensureConnected();
282
- return this.getScreencastFrames(params);
283
- case "upload_file":
284
- await this.ensureConnected();
285
- await this.ensureNoCaptcha("upload_file");
286
- return this.uploadFile(params);
287
- case "configure":
288
- await this.ensureConnected();
289
- return this.configureBrowser(params);
290
- // ==================== AGENT-CENTRIC APIs ====================
291
- case "agent_action":
292
- await this.ensureConnected();
293
- await this.ensureNoCaptcha("agent_action");
294
- return this.agentAction(params);
295
- case "smart_navigate":
296
- await this.ensureConnected();
297
- return this.smartNavigate(params);
298
- case "observe_and_act":
299
- await this.ensureConnected();
300
- await this.ensureNoCaptcha("observe_and_act");
301
- return this.observeAndAct(params);
302
- case "agent_form_fill":
303
- await this.ensureConnected();
304
- await this.ensureNoCaptcha("agent_form_fill");
305
- return this.agentFormFill(params);
306
- case "page_snapshot":
307
- await this.ensureConnected();
308
- return this.pageSnapshot(params);
309
- default:
310
- await this.ensureConnected();
311
- // Try as raw CDP command
312
- return this.client.sendCommand(method, params);
313
- }
314
- }
315
- async connect(params) {
316
- const port = params.port || 9222;
317
- await this.client.connect(port);
318
- return "Connected to browser";
319
- }
320
- async browserStatus(params) {
321
- const connected = this.client.isConnected();
322
- const activeTarget = this.client.getActiveTarget();
323
- let targets;
324
- if (connected && params.includeTargets) {
325
- targets = await this.client.getTabs().catch(() => []);
326
- }
327
- return {
328
- connected,
329
- activeTarget: activeTarget ? {
330
- id: activeTarget.id,
331
- type: activeTarget.type,
332
- title: activeTarget.title,
333
- url: activeTarget.url
334
- } : null,
335
- targets
336
- };
341
+ async launchBrowser(options) {
342
+ return Session.launchBrowser(this.client, options);
337
343
  }
338
- async snapshotCompact(params) {
339
- return this.snapshotCache.compact({
340
- maxElements: params.maxElements ?? 30,
341
- includeText: params.includeText !== false,
342
- });
344
+ async killBrowser() {
345
+ return Session.killBrowser(this.client);
343
346
  }
344
- async listInteractiveElements(params) {
345
- const maxElements = Math.max(0, Math.min(Number(params.maxElements ?? 50), 200));
346
- const snapshot = await this.snapshotCache.compact({
347
- maxElements,
348
- includeText: true,
349
- withOverlay: !!params.withOverlay,
350
- });
351
- return {
352
- count: snapshot.elements.length,
353
- cache: snapshot.cache,
354
- elements: snapshot.elements
355
- };
347
+ async listBrowsers() {
348
+ return Session.listBrowsers(this.client);
356
349
  }
357
- async getCompactElements(maxElements, includeText, withOverlay) {
358
- const result = await this.client.sendCommand("Runtime.evaluate", {
359
- expression: `
360
- (function() {
361
- const max = ${JSON.stringify(maxElements)};
362
- const includeText = ${JSON.stringify(includeText)};
363
- const selectors = [
364
- 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
365
- '[onclick]', '[role="button"]', '[role="link"]', '[role="checkbox"]',
366
- '[tabindex]:not([tabindex="-1"])', 'label', 'summary'
367
- ].join(', ');
368
-
369
- function cssPath(el) {
370
- if (el.id) return '#' + CSS.escape(el.id);
371
- const path = [];
372
- while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
373
- let selector = el.nodeName.toLowerCase();
374
- if (el.classList && el.classList.length) {
375
- selector += '.' + Array.from(el.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
376
- }
377
- const parent = el.parentElement;
378
- if (parent) {
379
- const siblings = Array.from(parent.children).filter(child => child.nodeName === el.nodeName);
380
- if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')';
381
- }
382
- path.unshift(selector);
383
- el = parent;
384
- }
385
- return path.length ? path.join(' > ') : '';
386
- }
387
-
388
- return Array.from(document.querySelectorAll(selectors)).map((el, index) => {
389
- const rect = el.getBoundingClientRect();
390
- const computed = window.getComputedStyle(el);
391
- const visible = computed.display !== 'none' && computed.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
392
- if (!visible) return null;
393
- const selector = cssPath(el);
394
- if (!selector) return null;
395
- const text = ((el.innerText || el.textContent || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '') + '').trim().replace(/\\s+/g, ' ').substring(0, 120);
396
- return {
397
- ref: 'css:' + selector,
398
- index: index + 1,
399
- tag: el.tagName.toLowerCase(),
400
- role: el.getAttribute('role') || '',
401
- type: el.getAttribute('type') || '',
402
- name: el.getAttribute('name') || '',
403
- text: includeText ? text : undefined,
404
- bounds: {
405
- x: Math.round(rect.left),
406
- y: Math.round(rect.top),
407
- width: Math.round(rect.width),
408
- height: Math.round(rect.height)
409
- }
410
- };
411
- }).filter(Boolean).slice(0, max);
412
- })()
413
- `,
414
- returnByValue: true,
415
- awaitPromise: true
416
- });
417
- const elements = result.result?.value || [];
418
- if (withOverlay && elements.length > 0) {
419
- await this.client.getInteractiveElements(true).catch(() => ({ elements: [], somInjected: false }));
420
- }
421
- return elements;
350
+ async listBrowserProfiles(browser) {
351
+ return Session.listBrowserProfiles(this.client, browser);
422
352
  }
423
- async clickByRef(params) {
424
- const ref = String(params.ref || "");
425
- if (!ref)
426
- throw new Error("ref required");
427
- if (ref.startsWith("css:")) {
428
- return this.clickBySelector({ selector: ref.slice(4), timeout: params.timeout });
429
- }
430
- if (ref.startsWith("point:")) {
431
- const [x, y] = ref.slice(6).split(",").map(Number);
432
- if (!Number.isFinite(x) || !Number.isFinite(y))
433
- throw new Error(`Invalid point ref: ${ref}`);
434
- const before = await this.captureActionFacts();
435
- await this.client.click(x, y);
436
- this.snapshotCache.invalidate("click_by_ref");
437
- const after = await this.captureActionFacts();
438
- return { success: true, ref, facts: this.diffActionFacts(before, after) };
439
- }
440
- if (ref.startsWith("@") || ref.startsWith("som:")) {
441
- const id = ref.replace(/^som:/, "").replace(/^@/, "");
442
- await this.clickElement({ id });
443
- return { success: true, ref };
353
+ // ─── Self-healing selector resolution ─────────────────────────────
354
+ async resolveSelector(params) {
355
+ const { originalSelector, text, fuzzyMatch } = params;
356
+ if (originalSelector) {
357
+ const exists = await this.client.evaluate(`!!document.querySelector(${JSON.stringify(originalSelector)})`);
358
+ if (exists) {
359
+ return { selector: originalSelector, method: "exact", confidence: 1.0 };
360
+ }
444
361
  }
445
- throw new Error(`Unsupported element ref: ${ref}`);
446
- }
447
- async clickBySelector(params) {
448
- const selector = params.selector;
449
- if (!selector)
450
- throw new Error("selector required");
451
- const found = await this.client.waitForSelector(selector, params.timeout || 5000, { visible: params.visible !== false, stable: params.stable === true });
452
- if (!found)
453
- return { success: false, selector, message: "Selector not found before timeout" };
454
- const before = await this.captureActionFacts();
455
- await this.clickElementBySelector({ selector });
456
- this.snapshotCache.invalidate("click_by_selector");
457
- const after = await this.captureActionFacts(selector);
458
- return { success: true, selector, facts: this.diffActionFacts(before, after) };
459
- }
460
- async fillBySelector(params) {
461
- const selector = params.selector;
462
- const value = params.value ?? "";
463
- if (!selector)
464
- throw new Error("selector required");
465
- const found = await this.client.waitForSelector(selector, params.timeout || 5000, { visible: params.visible !== false, stable: params.stable === true });
466
- if (!found)
467
- return { success: false, selector, message: "Selector not found before timeout" };
468
- await this.client.moveMouseToSelector(selector).catch(() => { });
469
- const focused = await this.client.evaluate(`
470
- (function() {
471
- const el = document.querySelector(${JSON.stringify(selector)});
472
- if (!el) return false;
473
- el.focus();
474
- if ('value' in el) {
475
- el.value = '';
476
- el.dispatchEvent(new Event('input', { bubbles: true }));
477
- el.dispatchEvent(new Event('change', { bubbles: true }));
478
- }
479
- return true;
480
- })()
481
- `);
482
- if (!focused)
483
- return { success: false, selector, message: "Selector could not be focused" };
484
- const before = await this.captureActionFacts(selector);
485
- await this.client.typeText(String(value));
486
- this.snapshotCache.invalidate("fill_by_selector");
487
- const after = await this.captureActionFacts(selector);
488
- return { success: true, selector, length: String(value).length, facts: this.diffActionFacts(before, after) };
489
- }
490
- async waitForSelectorCompact(params) {
491
- const selector = params.selector;
492
- if (!selector)
493
- throw new Error("selector required");
494
- const found = await this.client.waitForSelector(selector, params.timeout || 5000, { visible: params.visible === true, stable: params.stable === true });
495
- return { success: found, selector };
496
- }
497
- async getLogs(params) {
498
- const limit = Math.max(1, Math.min(Number(params.limit ?? 50), 100));
499
- const logs = await this.client.getConsoleLogs(limit);
500
- return { count: logs.length, logs };
501
- }
502
- async pressKey(params) {
503
- const key = String(params.key || params.value || "");
504
- if (!key)
505
- throw new Error("key required");
506
- const modifiers = Array.isArray(params.modifiers) ? params.modifiers.map(String) : [];
507
- const before = await this.captureActionFacts();
508
- await this.client.pressKey(key, modifiers);
509
- this.snapshotCache.invalidate("press_key");
510
- const after = await this.captureActionFacts();
511
- return { success: true, key, modifiers, facts: this.diffActionFacts(before, after) };
512
- }
513
- async clickText(params) {
514
- const resolved = await this.locator.resolve({
515
- target: params.text || params.value || params.target,
516
- role: params.role,
517
- timeout: params.timeout || 5000,
518
- includeCandidates: params.includeCandidates
519
- });
520
- if (!resolved.success)
521
- return resolved;
522
- const before = await this.captureActionFacts();
523
- await this.clickResolvedLocator(resolved.candidate);
524
- this.snapshotCache.invalidate("click_text");
525
- const after = await this.captureActionFacts(resolved.selector);
526
- return { success: true, selector: resolved.selector, ref: resolved.ref, matchedBy: resolved.matchedBy, confidence: resolved.confidence, facts: this.diffActionFacts(before, after) };
527
- }
528
- async clickRole(params) {
529
- const resolved = await this.locator.resolve({
530
- target: params.name || params.text || params.target || "",
531
- role: params.role,
532
- timeout: params.timeout || 5000,
533
- includeCandidates: params.includeCandidates
534
- });
535
- if (!resolved.success)
536
- return resolved;
537
- const before = await this.captureActionFacts();
538
- await this.clickResolvedLocator(resolved.candidate);
539
- this.snapshotCache.invalidate("click_role");
540
- const after = await this.captureActionFacts(resolved.selector);
541
- return { success: true, selector: resolved.selector, ref: resolved.ref, matchedBy: resolved.matchedBy, confidence: resolved.confidence, facts: this.diffActionFacts(before, after) };
542
- }
543
- async fillLabel(params) {
544
362
  const resolved = await this.locator.resolve({
545
- target: params.label || params.target,
546
- role: params.role || "textbox",
547
- timeout: params.timeout || 5000,
548
- includeCandidates: params.includeCandidates
549
- });
550
- if (!resolved.success)
551
- return resolved;
552
- if (resolved.candidate?.scope === "document" && resolved.candidate.framePath.length === 0 && resolved.candidate.shadowDepth === 0) {
553
- return this.fillBySelector({ selector: resolved.selector, value: params.value ?? "", timeout: params.timeout || 5000 });
554
- }
555
- const before = await this.captureActionFacts();
556
- await this.locator.focusAndClear(resolved.candidate);
557
- await this.client.typeText(String(params.value ?? ""));
558
- this.snapshotCache.invalidate("fill_label");
559
- const after = await this.captureActionFacts();
560
- return { success: true, selector: resolved.selector, ref: resolved.ref, matchedBy: resolved.matchedBy, confidence: resolved.confidence, length: String(params.value ?? "").length, facts: this.diffActionFacts(before, after) };
561
- }
562
- async clickResolvedLocator(candidate) {
563
- if (!candidate)
564
- throw new Error("Resolved locator missing candidate details");
565
- if (candidate.scope === "document" && candidate.framePath.length === 0 && candidate.shadowDepth === 0 && candidate.selector) {
566
- await this.clickElementBySelector({ selector: candidate.selector });
567
- return;
363
+ target: text,
364
+ timeout: params.timeout || 1500,
365
+ includeCandidates: false,
366
+ }).catch(() => null);
367
+ if (resolved?.success && (resolved.selector || resolved.ref)) {
368
+ return {
369
+ selector: resolved.candidate?.scope === "document" &&
370
+ resolved.candidate.framePath.length === 0 &&
371
+ resolved.candidate.shadowDepth === 0
372
+ ? resolved.selector || ""
373
+ : resolved.ref || "",
374
+ method: resolved.matchedBy || "locator",
375
+ confidence: resolved.confidence || 0.7,
376
+ };
568
377
  }
569
- await this.locator.click(candidate);
570
- }
571
- async elementAtPoint(params) {
572
- const x = Number(params.x ?? String(params.coordinate || "").split(",")[0]);
573
- const y = Number(params.y ?? String(params.coordinate || "").split(",")[1]);
574
- if (!Number.isFinite(x) || !Number.isFinite(y))
575
- throw new Error("x/y or coordinate required");
576
- return await this.client.evaluate(`
577
- (function() {
578
- const el = document.elementFromPoint(${JSON.stringify(x)}, ${JSON.stringify(y)});
579
- if (!el) return { found: false };
580
- const rect = el.getBoundingClientRect();
581
- function cssPath(node) {
582
- if (node.id) return '#' + CSS.escape(node.id);
583
- const path = [];
584
- while (node && node.nodeType === Node.ELEMENT_NODE && node !== document.body) {
585
- let selector = node.nodeName.toLowerCase();
586
- if (node.classList && node.classList.length) {
587
- selector += '.' + Array.from(node.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
588
- }
589
- const parent = node.parentElement;
590
- if (parent) {
591
- const siblings = Array.from(parent.children).filter(child => child.nodeName === node.nodeName);
592
- if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
593
- }
594
- path.unshift(selector);
595
- node = parent;
596
- }
597
- return path.join(' > ');
598
- }
599
- return {
600
- found: true,
601
- selector: cssPath(el),
602
- tag: el.tagName.toLowerCase(),
603
- text: String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ').substring(0, 160),
604
- role: el.getAttribute('role') || '',
605
- bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
606
- };
607
- })()
608
- `);
609
- }
610
- async waitForText(params) {
611
- const text = String(params.text || "");
612
- if (!text)
613
- throw new Error("text required");
614
- const timeout = params.timeout || 5000;
615
- const start = Date.now();
616
- while (Date.now() - start < timeout) {
617
- const found = await this.client.evaluate(`
618
- (document.body && document.body.innerText || '').includes(${JSON.stringify(text)})
619
- `).catch(() => false);
620
- if (found)
621
- return { success: true, text };
622
- await new Promise(r => setTimeout(r, 200));
378
+ if (fuzzyMatch !== false && text) {
379
+ const { makeFuzzyMatchScript } = await Promise.resolve().then(() => __importStar(require("./eval-scripts")));
380
+ const result = await this.client.evaluate(makeFuzzyMatchScript(text));
381
+ if (result) {
382
+ return { selector: result.selector, method: "fuzzy", confidence: result.confidence };
383
+ }
623
384
  }
624
- return { success: false, text, message: "Text not found before timeout" };
625
- }
626
- async getNetworkErrors(params) {
627
- const limit = Math.max(1, Math.min(Number(params.limit ?? 20), 100));
628
- const errors = (await this.client.getNetworkTraffic())
629
- .filter((entry) => entry.type === "error" || entry.status >= 400)
630
- .slice(-limit);
631
- return {
632
- count: errors.length,
633
- errors
634
- };
385
+ throw new Error(`Could not resolve selector. Original: ${originalSelector}, Text: ${text}`);
635
386
  }
387
+ // ─── CAPTCHA ──────────────────────────────────────────────────────
636
388
  async detectCaptcha() {
637
- return await this.client.evaluate(`
638
- (function() {
639
- const selectors = [
640
- 'iframe[src*="recaptcha"]',
641
- 'iframe[src*="hcaptcha"]',
642
- 'iframe[src*="challenges.cloudflare.com"]',
643
- 'iframe[src*="arkoselabs"]',
644
- 'iframe[src*="funcaptcha"]',
645
- '[class*="g-recaptcha"]',
646
- '[class*="h-captcha"]',
647
- '[data-sitekey]',
648
- '[id*="captcha" i]',
649
- '[class*="captcha" i]',
650
- '[aria-label*="captcha" i]'
651
- ];
652
- const textPatterns = [
653
- /captcha/i,
654
- /i am not a robot/i,
655
- /verify you are human/i,
656
- /verify that you are human/i,
657
- /security check/i,
658
- /human verification/i,
659
- /cloudflare.*verify/i
660
- ];
661
-
662
- function visible(el) {
663
- const rect = el.getBoundingClientRect();
664
- const style = window.getComputedStyle(el);
665
- return style.display !== 'none' &&
666
- style.visibility !== 'hidden' &&
667
- rect.width > 0 &&
668
- rect.height > 0;
669
- }
670
-
671
- const selectorMatches = selectors.flatMap((selector) =>
672
- Array.from(document.querySelectorAll(selector))
673
- .filter(visible)
674
- .slice(0, 5)
675
- .map((el) => {
676
- const rect = el.getBoundingClientRect();
677
- return {
678
- selector,
679
- tag: el.tagName.toLowerCase(),
680
- text: String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ').substring(0, 160),
681
- src: el.getAttribute('src') || '',
682
- bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
683
- };
684
- })
685
- );
686
-
687
- const bodyText = String(document.body?.innerText || '').replace(/\\s+/g, ' ').substring(0, 5000);
688
- const textMatches = textPatterns
689
- .filter((pattern) => pattern.test(bodyText))
690
- .map((pattern) => pattern.toString());
691
-
692
- const detected = selectorMatches.length > 0 || textMatches.length > 0;
693
- return {
694
- detected,
695
- captchaRequired: detected,
696
- message: detected ? 'CAPTCHA detected. Manual solve required before continuing.' : 'No CAPTCHA detected.',
697
- matches: selectorMatches,
698
- textMatches,
699
- url: window.location.href,
700
- title: document.title
701
- };
702
- })()
703
- `).catch((error) => ({
389
+ return await this.client.evaluate(eval_scripts_1.CAPTCHA_DETECTION_SCRIPT).catch((error) => ({
704
390
  detected: false,
705
391
  captchaRequired: false,
706
- message: `CAPTCHA detection failed: ${error.message}`
392
+ message: `CAPTCHA detection failed: ${error.message}`,
707
393
  }));
708
394
  }
709
- async ensureNoCaptcha(action) {
710
- const result = await this.detectCaptcha();
711
- if (result.detected) {
712
- const error = new Error("CAPTCHA detected. Manual solve required before continuing.");
713
- error.captcha = {
714
- ...result,
715
- blockedAction: action
716
- };
717
- throw error;
718
- }
719
- }
720
395
  async solveCaptchaAction(params) {
721
- const pageUrl = params.pageUrl || await this.client.evaluate("window.location.href").catch(() => "");
396
+ const pageUrl = params.pageUrl || (await this.client.evaluate("window.location.href").catch(() => ""));
722
397
  const opts = {
723
398
  useService: params.useService,
724
399
  service: params.service,
@@ -732,6 +407,54 @@ class CdpBridge {
732
407
  const mouse = this.client.getMousePosition();
733
408
  return await (0, captcha_solver_1.detectAndSolve)(evaluate, sendCommand, pageUrl, mouse, opts);
734
409
  }
410
+ // ─── Screenshot ───────────────────────────────────────────────────
411
+ async screenshot(params) {
412
+ const format = params.format === "png" ? "png" : "jpeg";
413
+ const quality = params.quality || 80;
414
+ if (params.x !== undefined) {
415
+ const result = await this.client.sendCommand("Page.captureScreenshot", {
416
+ format,
417
+ quality,
418
+ clip: {
419
+ x: params.x,
420
+ y: params.y,
421
+ width: params.width || 100,
422
+ height: params.height || 100,
423
+ scale: 1,
424
+ },
425
+ });
426
+ return result.data;
427
+ }
428
+ return await this.client.screenshot(format, quality);
429
+ }
430
+ // ─── Wait helpers (compact versions kept here) ────────────────────
431
+ async waitForSelectorCompact(params) {
432
+ const selector = params.selector;
433
+ if (!selector)
434
+ throw new Error("selector required");
435
+ const found = await this.client.waitForSelector(selector, params.timeout || 5000, {
436
+ visible: params.visible === true,
437
+ stable: params.stable === true,
438
+ });
439
+ return { success: found, selector };
440
+ }
441
+ async waitForText(params) {
442
+ const text = String(params.text || "");
443
+ if (!text)
444
+ throw new Error("text required");
445
+ const timeout = params.timeout || 5000;
446
+ const start = Date.now();
447
+ while (Date.now() - start < timeout) {
448
+ const found = await this.client
449
+ .evaluate(`(document.body && document.body.innerText || '').includes(${JSON.stringify(text)})`)
450
+ .catch(() => false);
451
+ if (found)
452
+ return { success: true, text };
453
+ await new Promise((r) => setTimeout(r, 200 * Math.max(0.1, this.getSpeedMultiplier())));
454
+ }
455
+ return { success: false, text, message: "Text not found before timeout" };
456
+ }
457
+ // ─── Browser Intent ───────────────────────────────────────────────
735
458
  async browserIntent(params) {
736
459
  const intent = String(params.intent || "").toLowerCase();
737
460
  const timeout = params.timeout || 7000;
@@ -739,495 +462,101 @@ class CdpBridge {
739
462
  const url = params.value || params.target;
740
463
  if (!url)
741
464
  throw new Error("value or target required for navigate intent");
742
- await this.navigate({ url: String(url), timeout });
743
- return this.intentResult(true, intent, undefined, { url });
465
+ await Navigation.navigate(this.client, { url: String(url), timeout }, this.snapshotCache);
466
+ return { success: true, intent, url };
744
467
  }
745
468
  if (intent === "inspect") {
746
- const snapshot = await this.snapshotCompact({ maxElements: params.maxElements ?? 30, includeText: true });
747
- return this.intentResult(true, intent, undefined, snapshot);
469
+ const snapshot = await Inspection.snapshotCompact(this.client, this.snapshotCache, {
470
+ maxElements: params.maxElements ?? 30,
471
+ includeText: true,
472
+ });
473
+ return { success: true, intent, ...snapshot };
748
474
  }
749
475
  if (intent === "wait_for") {
750
476
  const expected = params.value || params.target;
751
477
  if (!expected)
752
478
  throw new Error("value or target required for wait_for intent");
753
479
  const result = await this.waitForText({ text: expected, timeout });
754
- return this.intentResult(result.success, intent, undefined, result);
480
+ return { success: result.success, intent, ...result };
755
481
  }
756
482
  const resolved = await this.locator.resolve({
757
483
  target: params.target,
758
484
  role: params.role,
759
485
  timeout,
760
- includeCandidates: params.includeCandidates
486
+ includeCandidates: params.includeCandidates,
761
487
  });
762
488
  if (!resolved.success) {
763
- return this.intentResult(false, intent, undefined, {
489
+ return {
490
+ success: false,
491
+ intent,
764
492
  message: resolved.message,
765
- candidates: params.includeCandidates ? resolved.candidates : undefined
766
- });
493
+ candidates: params.includeCandidates ? resolved.candidates : undefined,
494
+ };
767
495
  }
768
496
  const selector = resolved.selector;
497
+ const candidate = resolved.candidate;
769
498
  if (intent === "click") {
770
- await this.clickResolvedLocator(resolved.candidate);
499
+ await Interaction.clickResolvedLocator(this.client, this.locator, this.snapshotCache, candidate);
771
500
  }
772
501
  else if (intent === "fill") {
773
- if (resolved.candidate?.scope === "document" && resolved.candidate.framePath.length === 0 && resolved.candidate.shadowDepth === 0) {
774
- await this.fillBySelector({ selector, value: params.value ?? "", timeout });
502
+ if (candidate?.scope === "document" && candidate.framePath.length === 0 && candidate.shadowDepth === 0 && selector) {
503
+ await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, {
504
+ selector: selector,
505
+ value: params.value ?? "",
506
+ timeout,
507
+ }, this.logger);
775
508
  }
776
509
  else {
777
- await this.locator.focusAndClear(resolved.candidate);
510
+ await this.locator.focusAndClear(candidate);
778
511
  await this.client.typeText(String(params.value ?? ""));
779
512
  }
780
513
  }
781
514
  else if (intent === "select") {
782
- await this.selectOption({ selector, value: params.value ?? "" });
515
+ await Interaction.selectOption(this.client, this.locator, this.snapshotCache, {
516
+ selector: selector,
517
+ value: params.value ?? "",
518
+ }, this.logger);
783
519
  }
784
520
  else if (intent === "check") {
785
- await this.checkElement({ selector });
521
+ await Interaction.checkElement(this.client, this.locator, this.snapshotCache, {
522
+ selector: selector,
523
+ }, this.logger);
786
524
  }
787
525
  else {
788
526
  throw new Error(`Unsupported browser intent: ${intent}`);
789
527
  }
790
- this.snapshotCache.invalidate(`browser_intent:${intent}`);
791
528
  let verification = undefined;
792
529
  if (params.verify) {
793
530
  verification = await this.waitForText({ text: params.verify, timeout }).catch((error) => ({
794
531
  success: false,
795
- error: error.message
532
+ error: error.message,
796
533
  }));
797
534
  }
798
- return this.intentResult(true, intent, resolved, {
535
+ return {
536
+ success: true,
537
+ intent,
538
+ target: resolved.target,
539
+ matchedBy: resolved.matchedBy,
540
+ confidence: resolved.confidence,
799
541
  selector,
800
542
  ref: resolved.ref || (selector ? `css:${selector}` : undefined),
801
543
  verification,
802
- candidates: params.includeCandidates ? resolved.candidates : undefined
803
- });
804
- }
805
- intentResult(success, intent, resolved, extra = {}) {
806
- return {
807
- success,
808
- intent,
809
- target: resolved?.target,
810
- matchedBy: resolved?.matchedBy,
811
- confidence: resolved?.confidence,
812
- ...extra
813
- };
814
- }
815
- async captureActionFacts(selector) {
816
- return await this.client.evaluate(`
817
- (function() {
818
- const selector = ${JSON.stringify(selector || "")};
819
- const active = document.activeElement;
820
- const target = selector ? document.querySelector(selector) : active;
821
- const visibleErrors = Array.from(document.querySelectorAll('[role="alert"], .error, .errors, [aria-invalid="true"]'))
822
- .filter((el) => {
823
- const rect = el.getBoundingClientRect();
824
- const style = window.getComputedStyle(el);
825
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
826
- })
827
- .map((el) => String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' '))
828
- .filter(Boolean)
829
- .slice(0, 5);
830
-
831
- function describe(el) {
832
- if (!el) return null;
833
- return {
834
- tag: el.tagName.toLowerCase(),
835
- id: el.id || '',
836
- name: el.getAttribute('name') || '',
837
- role: el.getAttribute('role') || '',
838
- type: el.getAttribute('type') || '',
839
- value: 'value' in el ? String(el.value || '') : '',
840
- checked: 'checked' in el ? !!el.checked : undefined,
841
- selectedIndex: 'selectedIndex' in el ? el.selectedIndex : undefined,
842
- text: String(el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').substring(0, 160)
843
- };
844
- }
845
-
846
- return {
847
- url: window.location.href,
848
- title: document.title,
849
- readyState: document.readyState,
850
- focused: describe(active),
851
- target: describe(target),
852
- visibleErrors
853
- };
854
- })()
855
- `).catch(() => ({}));
856
- }
857
- diffActionFacts(before, after) {
858
- return {
859
- urlChanged: before?.url !== after?.url,
860
- titleChanged: before?.title !== after?.title,
861
- focused: after?.focused,
862
- target: after?.target,
863
- valueChanged: before?.target?.value !== after?.target?.value,
864
- checkedChanged: before?.target?.checked !== after?.target?.checked,
865
- selectedIndexChanged: before?.target?.selectedIndex !== after?.target?.selectedIndex,
866
- visibleErrors: after?.visibleErrors || []
867
- };
868
- }
869
- async resolveNaturalTarget(params) {
870
- const target = String(params.target || "").trim();
871
- const role = params.role ? String(params.role).toLowerCase() : "";
872
- const timeout = params.timeout || 7000;
873
- const start = Date.now();
874
- if (!target && !role) {
875
- return { success: false, message: "target or role required" };
876
- }
877
- while (Date.now() - start < timeout) {
878
- const result = await this.client.sendCommand("Runtime.evaluate", {
879
- expression: `
880
- (function() {
881
- const target = ${JSON.stringify(target)};
882
- const targetLower = target.toLowerCase();
883
- const roleHint = ${JSON.stringify(role)};
884
- const selectors = [
885
- 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
886
- '[onclick]', '[role]', '[tabindex]:not([tabindex="-1"])', 'label', 'summary'
887
- ].join(', ');
888
-
889
- function cssPath(el) {
890
- if (el.id) return '#' + CSS.escape(el.id);
891
- const path = [];
892
- while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
893
- let selector = el.nodeName.toLowerCase();
894
- if (el.classList && el.classList.length) {
895
- selector += '.' + Array.from(el.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
896
- }
897
- const parent = el.parentElement;
898
- if (parent) {
899
- const siblings = Array.from(parent.children).filter(child => child.nodeName === el.nodeName);
900
- if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')';
901
- }
902
- path.unshift(selector);
903
- el = parent;
904
- }
905
- return path.length ? path.join(' > ') : '';
906
- }
907
-
908
- function visible(el) {
909
- const rect = el.getBoundingClientRect();
910
- const computed = window.getComputedStyle(el);
911
- return computed.display !== 'none' &&
912
- computed.visibility !== 'hidden' &&
913
- computed.opacity !== '0' &&
914
- rect.width > 0 &&
915
- rect.height > 0;
916
- }
917
-
918
- function inferRole(el) {
919
- const explicit = (el.getAttribute('role') || '').toLowerCase();
920
- if (explicit) return explicit;
921
- const tag = el.tagName.toLowerCase();
922
- const type = (el.getAttribute('type') || '').toLowerCase();
923
- if (tag === 'button' || type === 'button' || type === 'submit') return 'button';
924
- if (tag === 'a') return 'link';
925
- if (tag === 'textarea') return 'textbox';
926
- if (tag === 'select') return 'combobox';
927
- if (tag === 'input' && ['checkbox', 'radio'].includes(type)) return type;
928
- if (tag === 'input') return 'textbox';
929
- return tag;
930
- }
931
-
932
- function labelFor(el) {
933
- if (el.id) {
934
- const label = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
935
- if (label) return label.innerText || label.textContent || '';
936
- }
937
- const wrappingLabel = el.closest('label');
938
- return wrappingLabel ? (wrappingLabel.innerText || wrappingLabel.textContent || '') : '';
939
- }
940
-
941
- function norm(value) {
942
- return String(value || '').trim().replace(/\\s+/g, ' ');
943
- }
944
-
945
- function scoreField(value, weightExact, weightIncludes) {
946
- const text = norm(value);
947
- const lower = text.toLowerCase();
948
- if (!targetLower) return { score: 0, by: '' };
949
- if (lower === targetLower) return { score: weightExact, by: 'exact' };
950
- if (lower.includes(targetLower)) return { score: weightIncludes, by: 'contains' };
951
- if (targetLower.includes(lower) && lower.length >= 3) return { score: Math.max(1, weightIncludes - 1), by: 'contained_by_target' };
952
- return { score: 0, by: '' };
953
- }
954
-
955
- const candidates = Array.from(document.querySelectorAll(selectors)).map((el) => {
956
- if (!visible(el)) return null;
957
- const selector = cssPath(el);
958
- if (!selector) return null;
959
-
960
- const inferredRole = inferRole(el);
961
- const fields = [
962
- ['selector', selector, 12, 10],
963
- ['aria-label', el.getAttribute('aria-label'), 11, 9],
964
- ['label', labelFor(el), 11, 9],
965
- ['placeholder', el.getAttribute('placeholder'), 10, 8],
966
- ['name', el.getAttribute('name'), 9, 7],
967
- ['text', el.innerText || el.textContent, 8, 6],
968
- ['value', el.getAttribute('value'), 7, 5],
969
- ['title', el.getAttribute('title'), 6, 4]
970
- ];
971
-
972
- let score = 0;
973
- let matchedBy = '';
974
- for (const [field, value, exact, includes] of fields) {
975
- const match = scoreField(value, exact, includes);
976
- if (match.score > score) {
977
- score = match.score;
978
- matchedBy = field + ':' + match.by;
979
- }
980
- }
981
-
982
- if (roleHint) {
983
- if (inferredRole === roleHint) score += 4;
984
- else if (roleHint === 'textbox' && ['input', 'textarea'].includes(el.tagName.toLowerCase())) score += 3;
985
- else score -= 2;
986
- }
987
-
988
- const rect = el.getBoundingClientRect();
989
- return {
990
- selector,
991
- role: inferredRole,
992
- tag: el.tagName.toLowerCase(),
993
- type: el.getAttribute('type') || '',
994
- text: norm(el.innerText || el.textContent || el.getAttribute('aria-label') || el.getAttribute('placeholder')).substring(0, 120),
995
- matchedBy,
996
- score,
997
- bounds: {
998
- x: Math.round(rect.left),
999
- y: Math.round(rect.top),
1000
- width: Math.round(rect.width),
1001
- height: Math.round(rect.height)
1002
- }
1003
- };
1004
- }).filter(Boolean).sort((a, b) => b.score - a.score);
1005
-
1006
- return candidates;
1007
- })()
1008
- `,
1009
- returnByValue: true,
1010
- awaitPromise: true
1011
- });
1012
- const candidates = result.result?.value || [];
1013
- const best = candidates[0];
1014
- if (best && best.score > 0) {
1015
- return {
1016
- success: true,
1017
- target,
1018
- selector: best.selector,
1019
- matchedBy: best.matchedBy,
1020
- confidence: Math.min(1, best.score / 16),
1021
- candidates: params.includeCandidates ? candidates.slice(0, 10) : undefined
1022
- };
1023
- }
1024
- await new Promise(r => setTimeout(r, 200));
1025
- }
1026
- return { success: false, target, message: "No matching visible element found" };
1027
- }
1028
- async getState(params) {
1029
- const includeScreenshot = params.screenshot === true;
1030
- const includeDomSnapshot = params.domSnapshot === true || params.includeDOMSnapshot === true;
1031
- const includeElements = params.elements !== false;
1032
- const includeSoM = params.som === true || params.withOverlay === true;
1033
- const includeTabs = params.tabs === true;
1034
- const compact = includeElements
1035
- ? await this.snapshotCache.compact({ maxElements: 200, includeText: true, withOverlay: includeSoM })
1036
- : await this.snapshotCache.compact({ maxElements: 0, includeText: false });
1037
- const [screenshot, domSnapshot, tabs] = await Promise.all([
1038
- includeScreenshot ? this.client.screenshot(params.format, params.quality).catch(() => null) : Promise.resolve(null),
1039
- includeDomSnapshot ? this.client.getDOMSnapshot().catch(() => null) : Promise.resolve(null),
1040
- includeTabs ? this.client.getTabs().catch(() => []) : Promise.resolve([]),
1041
- ]);
1042
- if (includeSoM) {
1043
- await this.client.removeSoMOverlay().catch(() => { });
1044
- }
1045
- return {
1046
- title: compact.title,
1047
- url: compact.url,
1048
- screenshot,
1049
- domSnapshot,
1050
- elements: includeElements ? compact.elements : [],
1051
- somInjected: includeSoM,
1052
- cache: compact.cache,
1053
- tabs,
544
+ candidates: params.includeCandidates ? resolved.candidates : undefined,
1054
545
  };
1055
546
  }
1056
- async navigate(params) {
1057
- await this.client.navigateAndWait(params.url, params.timeout || 10000);
1058
- this.snapshotCache.invalidate("navigate");
1059
- return "Navigated";
1060
- }
1061
- async waitForPageSettled(timeout = 10000) {
1062
- try {
1063
- await this.client.waitForNavigation(Math.min(timeout, 10000));
1064
- }
1065
- catch {
1066
- await this.client.waitForNetworkIdle(300, Math.min(timeout, 2500)).catch(() => { });
1067
- }
1068
- }
1069
- async click(params) {
1070
- const x = params.x || params.coordinate?.split(',')[0] || 100;
1071
- const y = params.y || params.coordinate?.split(',')[1] || 100;
1072
- await this.client.click(x, y);
1073
- this.snapshotCache.invalidate("click");
1074
- return "Clicked";
1075
- }
1076
- async clickElement(params) {
1077
- // Click by element ID (from SoM) - resolves ID to coordinates
1078
- if (params.id !== undefined) {
1079
- const result = await this.client.evaluate(`
1080
- (function() {
1081
- const targetId = Number(${JSON.stringify(String(params.id).replace(/@/g, ''))});
1082
- const selectors = [
1083
- 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
1084
- '[onclick]', '[role="button"]', '[role="link"]', '[role="checkbox"]',
1085
- '[tabindex]:not([tabindex="-1"])', 'label', 'summary'
1086
- ].join(', ');
1087
- const elements = Array.from(document.querySelectorAll(selectors)).filter((el) => {
1088
- const rect = el.getBoundingClientRect();
1089
- const computed = window.getComputedStyle(el);
1090
- return computed.display !== 'none' &&
1091
- computed.visibility !== 'hidden' &&
1092
- rect.width > 0 &&
1093
- rect.height > 0;
1094
- });
1095
- const el = elements[targetId - 1];
1096
- if (el) {
1097
- el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
1098
- const rect = el.getBoundingClientRect();
1099
- return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, w: rect.width };
1100
- }
1101
- return null;
1102
- })()
1103
- `);
1104
- if (result) {
1105
- await this.client.click(result.x, result.y, params.button, result.w);
1106
- this.snapshotCache.invalidate("click_element");
1107
- return `Clicked element @${params.id}`;
1108
- }
1109
- // Fallback: try to find element by selector or text
1110
- if (params.selector) {
1111
- return this.clickElementBySelector({ selector: params.selector });
1112
- }
1113
- if (params.text) {
1114
- return this.clickElementByText({ text: params.text });
1115
- }
1116
- }
1117
- // Fallback to coordinate click
1118
- if (params.x !== undefined && params.y !== undefined) {
1119
- await this.client.click(params.x, params.y);
1120
- this.snapshotCache.invalidate("click_element");
1121
- return "Clicked at coordinates";
1122
- }
1123
- throw new Error("Element not found: no valid id, selector, text, or coordinates provided");
1124
- }
1125
- async clickElementByText(params) {
1126
- const result = await this.locator.resolve({ target: params.text, timeout: params.timeout || 5000 });
1127
- if (result.success && result.candidate) {
1128
- await this.clickResolvedLocator(result.candidate);
1129
- this.snapshotCache.invalidate("click_element_by_text");
1130
- return `Clicked element with text: ${params.text}`;
1131
- }
1132
- throw new Error(`Element with text not found: ${params.text}`);
1133
- }
1134
- async clickElementBySelector(params) {
1135
- const selector = params.selector;
1136
- if (!selector)
1137
- throw new Error("Selector required");
1138
- if (String(selector).startsWith("point:")) {
1139
- const [x, y] = String(selector).slice(6).split(",").map(Number);
1140
- if (!Number.isFinite(x) || !Number.isFinite(y))
1141
- throw new Error(`Invalid point selector: ${selector}`);
1142
- await this.client.click(x, y);
1143
- this.snapshotCache.invalidate("click_element_by_selector");
1144
- return "Clicked element by point";
1145
- }
1146
- // Get element bounds via CDP
1147
- const result = await this.client.sendCommand("Runtime.evaluate", {
1148
- expression: `
1149
- (function() {
1150
- const el = document.querySelector(${JSON.stringify(selector)});
1151
- if (!el) return null;
1152
- el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
1153
- const rect = el.getBoundingClientRect();
1154
- const computed = window.getComputedStyle(el);
1155
- if (computed.display === 'none' || computed.visibility === 'hidden' || rect.width === 0 || rect.height === 0) return null;
1156
- return { x: rect.left + rect.width/2, y: rect.top + rect.height/2, w: rect.width };
1157
- })()
1158
- `,
1159
- returnByValue: true,
1160
- });
1161
- if (result.result?.value) {
1162
- const { x, y, w } = result.result.value;
1163
- await this.client.click(x, y, params.button, w);
1164
- this.snapshotCache.invalidate("click_element_by_selector");
1165
- return "Clicked element by selector";
1166
- }
1167
- throw new Error(`Element not found: ${selector}`);
1168
- }
1169
- async type(params) {
1170
- const text = params.text || params.value || "";
1171
- await this.client.typeText(text);
1172
- return "Typed text";
1173
- }
1174
- async evaluate(params) {
1175
- return await this.client.evaluate(params.script);
1176
- }
1177
- async screenshot(params) {
1178
- const format = params.format === "png" ? "png" : "jpeg";
1179
- const quality = params.quality || 80;
1180
- if (params.x !== undefined) {
1181
- // Region screenshot - use CDP clipping
1182
- const result = await this.client.sendCommand("Page.captureScreenshot", {
1183
- format,
1184
- quality,
1185
- clip: {
1186
- x: params.x,
1187
- y: params.y,
1188
- width: params.width || 100,
1189
- height: params.height || 100,
1190
- scale: 1,
1191
- },
1192
- });
1193
- return result.data;
1194
- }
1195
- return await this.client.screenshot(format, quality);
1196
- }
1197
- async scroll(params) {
1198
- const x = params.x || 0;
1199
- const y = params.y || 0;
1200
- const originX = params.originX ?? params.mouseX ?? params.options?.originX;
1201
- const originY = params.originY ?? params.mouseY ?? params.options?.originY;
1202
- await this.client.wheel(x, y, originX, originY);
1203
- return "Scrolled";
1204
- }
1205
- async wait(params) {
1206
- const ms = params.ms || params.timeout || 1000;
1207
- await new Promise((r) => setTimeout(r, ms));
1208
- return "Waited";
1209
- }
1210
- async cdpCommand(params) {
1211
- return await this.client.sendCommand(params.command, params.args || {});
1212
- }
1213
- async getDomSnapshot(params) {
1214
- return await this.client.getDOMSnapshot();
1215
- }
1216
- // ==================== AGENT-CENTRIC APIs ====================
547
+ // ─── Agent APIs ───────────────────────────────────────────────────
1217
548
  async agentAction(params) {
1218
549
  const { action, target, verify, waitFor, timeout } = params;
1219
550
  const timeoutMs = timeout || 10000;
1220
551
  try {
1221
- // Execute action with proper element resolution
1222
552
  switch (action) {
1223
553
  case "click":
1224
554
  if (target.id) {
1225
- await this.clickElement({ id: target.id, button: target.button });
555
+ await Interaction.clickElement(this.client, this.locator, this.snapshotCache, { id: target.id, button: target.button }, this.logger);
1226
556
  }
1227
557
  else if (target.selector) {
1228
- // Wait for selector if needed
1229
558
  await this.client.waitForSelector(target.selector, 5000);
1230
- await this.clickElementBySelector({ selector: target.selector, button: target.button });
559
+ await Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, { selector: target.selector, button: target.button }, this.logger);
1231
560
  }
1232
561
  else if (target.x !== undefined) {
1233
562
  await this.client.click(target.x, target.y, target.button);
@@ -1237,28 +566,19 @@ class CdpBridge {
1237
566
  if (target.selector) {
1238
567
  await this.client.waitForSelector(target.selector, 5000);
1239
568
  await this.client.moveMouseToSelector(target.selector).catch(() => { });
1240
- await this.client.evaluate(`
1241
- (function() {
1242
- const el = document.querySelector(${JSON.stringify(target.selector)});
1243
- if (el) { el.value = ''; el.focus(); }
1244
- })()
1245
- `);
569
+ await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(target.selector)});if(el){el.value='';el.focus();}})()`);
1246
570
  }
1247
571
  await this.client.typeText(target.text || target.value || "");
1248
572
  break;
1249
573
  case "scroll":
1250
574
  await this.client.sendCommand("Input.dispatchMouseWheel", {
1251
575
  x: target.x || 0, y: target.y || 0,
1252
- deltaX: target.deltaX || 0, deltaY: target.deltaY || target.y || 0
576
+ deltaX: target.deltaX || 0, deltaY: target.deltaY || target.y || 0,
1253
577
  });
1254
578
  break;
1255
579
  case "key_press":
1256
- await this.client.sendCommand("Input.dispatchKeyEvent", {
1257
- type: "keyDown", text: target.key, key: target.key
1258
- });
1259
- await this.client.sendCommand("Input.dispatchKeyEvent", {
1260
- type: "keyUp", text: target.key, key: target.key
1261
- });
580
+ await this.client.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", text: target.key, key: target.key });
581
+ await this.client.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", text: target.key, key: target.key });
1262
582
  break;
1263
583
  case "hover":
1264
584
  await this.client.moveMouse(target.x || 0, target.y || 0);
@@ -1274,146 +594,56 @@ class CdpBridge {
1274
594
  await this.client.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x: ex, y: ey, button: "left", clickCount: 1 });
1275
595
  break;
1276
596
  }
1277
- // Wait for condition with proper waiting mechanisms
1278
597
  if (waitFor) {
1279
598
  if (waitFor.type === "network_idle") {
1280
599
  await this.client.waitForNetworkIdle(500, waitFor.timeout || 3000);
1281
600
  }
1282
- else if (waitFor.type === "element") {
1283
- if (waitFor.selector) {
1284
- await this.client.waitForSelector(waitFor.selector, waitFor.timeout || 5000);
1285
- }
1286
- else {
1287
- await new Promise(r => setTimeout(r, waitFor.timeout || 3000));
1288
- }
601
+ else if (waitFor.type === "element" && waitFor.selector) {
602
+ await this.client.waitForSelector(waitFor.selector, waitFor.timeout || 5000);
1289
603
  }
1290
604
  else if (waitFor.type === "navigation") {
1291
- try {
1292
- await this.client.waitForNavigation(waitFor.timeout || 10000);
1293
- }
1294
- catch {
1295
- // Navigation might have already completed
1296
- }
605
+ await this.client.waitForNavigation(waitFor.timeout || 10000).catch(() => { });
1297
606
  }
1298
607
  }
1299
608
  else {
1300
- // Default wait for stability
1301
- await new Promise(r => setTimeout(r, 500));
609
+ await new Promise((r) => setTimeout(r, 500));
1302
610
  }
1303
- // Verify if requested
1304
611
  let verification = null;
1305
612
  if (verify) {
1306
613
  if (verify.type === "element_exists") {
1307
- const res = await this.client.evaluate(`
1308
- (function() {
1309
- const el = document.querySelector(${JSON.stringify(verify.selector)});
1310
- return { success: !!el, exists: !!el, message: el ? "Element exists" : "Element not found" };
1311
- })()
1312
- `);
1313
- verification = res;
614
+ verification = await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(verify.selector)});return {success:!!el,exists:!!el,message:el?"Element exists":"Element not found"};})()`);
1314
615
  }
1315
616
  else if (verify.type === "element_contains_text") {
1316
- const res = await this.client.evaluate(`
1317
- (function() {
1318
- const el = document.querySelector(${JSON.stringify(verify.selector)});
1319
- if (!el) return { success: false, message: "Element not found" };
1320
- const text = (el.innerText || el.textContent || "").trim();
1321
- const matches = text.includes(${JSON.stringify(verify.expectedText || verify.text || "")});
1322
- return { success: matches, text, message: matches ? "Verified" : "Text mismatch" };
1323
- })()
1324
- `);
1325
- verification = res;
617
+ verification = await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(verify.selector)});if(!el)return {success:false,message:"Element not found"};var t=(el.innerText||el.textContent||"").trim();var m=t.indexOf(${JSON.stringify(verify.expectedText || verify.text || "")})>=0;return {success:m,text:t,message:m?"Verified":"Text mismatch"};})()`);
1326
618
  }
1327
619
  else if (verify.selector) {
1328
- // Simple existence check
1329
- const res = await this.client.evaluate(`
1330
- (function() {
1331
- return !!document.querySelector(${JSON.stringify(verify.selector)});
1332
- })()
1333
- `);
620
+ const res = await this.client.evaluate(`!!document.querySelector(${JSON.stringify(verify.selector)})`);
1334
621
  verification = { success: !!res, selector: verify.selector };
1335
622
  }
1336
623
  }
1337
- // Get screenshot
1338
624
  const screenshot = params.screenshot === true ? await this.client.screenshot("jpeg", 70).catch(() => null) : null;
1339
625
  const url = await this.client.evaluate("window.location.href").catch(() => "Unknown");
1340
626
  const title = await this.client.evaluate("document.title").catch(() => "Unknown");
1341
- return {
1342
- success: true,
1343
- action: `${action} completed`,
1344
- verification,
1345
- screenshot,
1346
- url,
1347
- title
1348
- };
627
+ return { success: true, action: `${action} completed`, verification, screenshot, url, title };
1349
628
  }
1350
629
  catch (e) {
1351
630
  return { success: false, error: e.message };
1352
631
  }
1353
632
  }
1354
- async smartNavigate(params) {
1355
- const { url, waitFor, dismissPopups, screenshot, timeout } = params;
1356
- const timeoutMs = timeout || 30000;
633
+ async observeAndAct(params) {
634
+ const { action, observe, returnScreenshot } = params;
1357
635
  try {
1358
- await this.client.navigateAndWait(url, timeoutMs);
1359
- // Dismiss popups if requested
1360
- if (dismissPopups !== false) {
1361
- await this.client.evaluate(`
1362
- (function() {
1363
- // Try to find and click common close buttons
1364
- const selectors = [
1365
- '[aria-label*="close" i]', '[aria-label*="dismiss" i]',
1366
- '.close', '.dismiss', '.modal-close',
1367
- 'button[class*="close"]', '[data-dismiss="modal"]'
1368
- ];
1369
- for (const sel of selectors) {
1370
- const el = document.querySelector(sel);
1371
- if (el && el.offsetParent !== null) {
1372
- el.click();
1373
- return true;
1374
- }
1375
- }
1376
- return false;
1377
- })()
1378
- `).catch(() => { });
636
+ const [beforeFacts, beforeScreenshot] = await Promise.all([
637
+ Interaction.captureActionFacts(this.client, action?.selector),
638
+ returnScreenshot === true ? this.client.screenshot("jpeg", 70).catch(() => null) : Promise.resolve(null),
639
+ ]);
640
+ if (action.type === "click" && action.selector) {
641
+ await this.client.waitForSelector(action.selector, 5000, { visible: true, stable: true });
642
+ await Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, { selector: action.selector }, this.logger);
1379
643
  }
1380
- // Wait for specific condition
1381
- if (waitFor) {
1382
- if (waitFor.type === "network_idle") {
1383
- await this.client.waitForNetworkIdle(500, waitFor.timeout || 3000);
1384
- }
1385
- else if (waitFor.type === "element" && waitFor.selector) {
1386
- await this.client.waitForSelector(waitFor.selector, waitFor.timeout || 5000);
1387
- }
1388
- }
1389
- const currentUrl = await this.client.evaluate("window.location.href").catch(() => url);
1390
- const title = await this.client.evaluate("document.title").catch(() => "Unknown");
1391
- const screenshotData = screenshot === true ? await this.client.screenshot("jpeg", 70).catch(() => null) : null;
1392
- return {
1393
- success: true,
1394
- url: currentUrl,
1395
- title,
1396
- screenshot: screenshotData
1397
- };
1398
- }
1399
- catch (e) {
1400
- return { success: false, error: e.message };
1401
- }
1402
- }
1403
- async observeAndAct(params) {
1404
- const { action, observe, returnScreenshot } = params;
1405
- try {
1406
- const [beforeFacts, beforeScreenshot] = await Promise.all([
1407
- this.captureActionFacts(action.selector),
1408
- returnScreenshot === true ? this.client.screenshot("jpeg", 70).catch(() => null) : Promise.resolve(null),
1409
- ]);
1410
- if (action.type === "click" && action.selector) {
1411
- await this.client.waitForSelector(action.selector, 5000, { visible: true, stable: true });
1412
- await this.clickElementBySelector({ selector: action.selector });
1413
- }
1414
- else if (action.type === "type" && action.text) {
1415
- if (action.selector) {
1416
- await this.fillBySelector({ selector: action.selector, value: action.text, timeout: 5000 });
644
+ else if (action.type === "type" && action.text) {
645
+ if (action.selector) {
646
+ await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, { selector: action.selector, value: action.text, timeout: 5000 }, this.logger);
1417
647
  }
1418
648
  else {
1419
649
  await this.client.typeText(action.text);
@@ -1426,20 +656,19 @@ class CdpBridge {
1426
656
  await this.client.waitForNetworkIdle(500, 5000).catch(() => { });
1427
657
  }
1428
658
  else {
1429
- await new Promise(r => setTimeout(r, 300));
659
+ await new Promise((r) => setTimeout(r, 300));
1430
660
  }
1431
661
  const [afterFacts, afterScreenshot] = await Promise.all([
1432
- this.captureActionFacts(action.selector),
662
+ Interaction.captureActionFacts(this.client, action?.selector),
1433
663
  returnScreenshot === true ? this.client.screenshot("jpeg", 70).catch(() => null) : Promise.resolve(null),
1434
664
  ]);
1435
- const facts = this.diffActionFacts(beforeFacts, afterFacts);
665
+ const facts = Interaction.diffActionFacts(beforeFacts, afterFacts);
1436
666
  const changesDetected = facts.urlChanged || facts.titleChanged || facts.valueChanged || facts.checkedChanged || facts.selectedIndexChanged;
1437
667
  return {
1438
668
  success: true,
1439
669
  before: { facts: beforeFacts, screenshot: beforeScreenshot },
1440
670
  after: { facts: afterFacts, screenshot: afterScreenshot },
1441
- changesDetected,
1442
- facts,
671
+ changesDetected, facts,
1443
672
  navigationOccurred: facts.urlChanged,
1444
673
  };
1445
674
  }
@@ -1447,18 +676,6 @@ class CdpBridge {
1447
676
  return { success: false, error: e.message };
1448
677
  }
1449
678
  }
1450
- simpleDiff(before, after) {
1451
- // Simple Levenshtein distance for change detection
1452
- if (before === after)
1453
- return 0;
1454
- const len = Math.max(before.length, after.length);
1455
- let diff = 0;
1456
- for (let i = 0; i < len; i++) {
1457
- if (before[i] !== after[i])
1458
- diff++;
1459
- }
1460
- return diff;
1461
- }
1462
679
  async agentFormFill(params) {
1463
680
  const { fields, submitAfterFill, submitSelector } = params;
1464
681
  const results = [];
@@ -1470,29 +687,29 @@ class CdpBridge {
1470
687
  continue;
1471
688
  }
1472
689
  if (["text", "email", "password", "textarea"].includes(field.type) || !field.type) {
1473
- await this.fillBySelector({ selector, value: field.value ?? "", timeout: field.timeout || 5000 });
690
+ await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, { selector, value: field.value ?? "", timeout: field.timeout || 5000 }, this.logger);
1474
691
  }
1475
692
  else if (field.type === "select") {
1476
- await this.selectOption({ selector, value: field.value ?? "" });
693
+ await Interaction.selectOption(this.client, this.locator, this.snapshotCache, { selector, value: field.value ?? "" }, this.logger);
1477
694
  }
1478
695
  else if (field.type === "checkbox" || field.type === "radio") {
1479
696
  if (field.checked === false) {
1480
- await this.setChecked({ selector, checked: false });
697
+ await Interaction.setChecked(this.client, this.locator, this.snapshotCache, { selector, checked: false }, this.logger);
1481
698
  }
1482
699
  else {
1483
- await this.checkElement({ selector });
700
+ await Interaction.checkElement(this.client, this.locator, this.snapshotCache, { selector }, this.logger);
1484
701
  }
1485
702
  }
1486
703
  else if (field.type === "file") {
1487
704
  await this.uploadFile({ selector, files: field.files || [] });
1488
705
  }
1489
706
  else {
1490
- await this.fillBySelector({ selector, value: field.value ?? "", timeout: field.timeout || 5000 });
707
+ await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, { selector, value: field.value ?? "", timeout: field.timeout || 5000 }, this.logger);
1491
708
  }
1492
709
  results.push({ field: field.id || field.selector, selector, success: true });
1493
710
  }
1494
711
  if (submitAfterFill && submitSelector) {
1495
- await this.clickElementBySelector({ selector: submitSelector });
712
+ await Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, { selector: submitSelector }, this.logger);
1496
713
  }
1497
714
  return { success: true, fieldsFilled: results.length, results };
1498
715
  }
@@ -1500,200 +717,129 @@ class CdpBridge {
1500
717
  return { success: false, error: e.message, results };
1501
718
  }
1502
719
  }
1503
- async pageSnapshot(params) {
1504
- try {
1505
- const includeScreenshot = params.screenshot === true;
1506
- const includeCookies = params.cookies === true;
1507
- const includeAccessibilityTree = params.accessibilityTree === true;
1508
- const [title, url, screenshot, domSnapshot, elements, forms, cookies, axTree] = await Promise.all([
1509
- this.client.evaluate("document.title").catch(() => "Unknown"),
1510
- this.client.evaluate("window.location.href").catch(() => "Unknown"),
1511
- includeScreenshot ? this.client.screenshot(params.fullPage ? "jpeg" : "jpeg", 70).catch(() => null) : Promise.resolve(null),
1512
- params.includeDOMSnapshot ? this.client.getDOMSnapshot().catch(() => null) : Promise.resolve(undefined),
1513
- this.client.getInteractiveElements(false).catch(() => ({ elements: [] })),
1514
- this.client.evaluate(`
1515
- (function() {
1516
- const forms = Array.from(document.querySelectorAll('form'));
1517
- return forms.map((form, idx) => {
1518
- const inputs = Array.from(form.querySelectorAll('input, select, textarea'));
1519
- return {
1520
- id: 'form-' + idx,
1521
- action: form.action,
1522
- method: form.method,
1523
- inputs: inputs.map(input => ({
1524
- type: input.type || 'text',
1525
- name: input.name || '',
1526
- id: input.id || '',
1527
- required: input.required,
1528
- placeholder: input.placeholder || ''
1529
- }))
1530
- };
1531
- });
1532
- })()
1533
- `).catch(() => ({ value: [] })),
1534
- includeCookies ? this.client.sendCommand("Network.getCookies", {}).catch(() => ({ cookies: [] })) : Promise.resolve({ cookies: [] }),
1535
- includeAccessibilityTree ? this.client.getSimplifiedAccessibilityTree().catch(() => []) : Promise.resolve([]),
1536
- ]);
1537
- return {
1538
- title,
1539
- url,
1540
- screenshot,
1541
- elements: elements.elements,
1542
- accessibilityTree: axTree,
1543
- forms: forms || [],
1544
- cookies: cookies.cookies || [],
1545
- domSnapshot: params.includeDOMSnapshot ? domSnapshot : undefined,
1546
- metadata: {
1547
- timestamp: Date.now(),
1548
- elementCount: elements.elements.length,
1549
- }
1550
- };
1551
- }
1552
- catch (e) {
1553
- return { success: false, error: e.message };
1554
- }
1555
- }
1556
- // ==================== SELF-HEALING SELECTOR RESOLUTION ====================
1557
- async resolveSelector(params) {
1558
- const { originalSelector, text, fuzzyMatch } = params;
1559
- // Try original selector first
1560
- if (originalSelector) {
1561
- const exists = await this.client.evaluate(`
1562
- !!document.querySelector(${JSON.stringify(originalSelector)})
1563
- `);
1564
- if (exists) {
1565
- return { selector: originalSelector, method: "exact", confidence: 1.0 };
720
+ // ─── Configuration ────────────────────────────────────────────────
721
+ async configureBrowser(params) {
722
+ const { network, emulation, script } = params;
723
+ const results = [];
724
+ if (network) {
725
+ if (network.blockImages) {
726
+ await this.client.sendCommand("Network.setBlockedURLs", {
727
+ urls: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.webp", "*.svg"],
728
+ });
729
+ results.push("Blocked images");
730
+ }
731
+ if (network.blockCSS) {
732
+ await this.client.sendCommand("Network.setBlockedURLs", { urls: ["*.css"] });
733
+ results.push("Blocked CSS");
734
+ }
735
+ if (network.blockAds) {
736
+ await this.client.sendCommand("Network.setBlockedURLs", {
737
+ urls: ["*doubleclick.net*", "*googlesyndication.com*", "*adservice.*"],
738
+ });
739
+ results.push("Blocked ads");
1566
740
  }
1567
741
  }
1568
- const resolved = await this.locator.resolve({
1569
- target: text,
1570
- timeout: params.timeout || 1500,
1571
- includeCandidates: false,
1572
- }).catch(() => null);
1573
- if (resolved?.success && (resolved.selector || resolved.ref)) {
1574
- return {
1575
- selector: resolved.candidate?.scope === "document" && resolved.candidate.framePath.length === 0 && resolved.candidate.shadowDepth === 0
1576
- ? resolved.selector || ""
1577
- : resolved.ref || "",
1578
- method: resolved.matchedBy || "locator",
1579
- confidence: resolved.confidence || 0.7,
1580
- };
1581
- }
1582
- // Try fuzzy text matching if enabled
1583
- if (fuzzyMatch !== false && text) {
1584
- const result = await this.client.evaluate(`
1585
- (function() {
1586
- const searchText = ${JSON.stringify(text)};
1587
- const searchLower = String(searchText || '').trim().toLowerCase();
1588
- const elements = Array.from(document.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [onclick]')).filter((el) => {
1589
- const rect = el.getBoundingClientRect();
1590
- const computed = window.getComputedStyle(el);
1591
- return computed.display !== 'none' && computed.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
1592
- });
1593
-
1594
- function cssPath(el) {
1595
- if (el.id) return '#' + CSS.escape(el.id);
1596
- const path = [];
1597
- while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
1598
- let selector = el.nodeName.toLowerCase();
1599
- if (el.classList && el.classList.length) {
1600
- selector += '.' + Array.from(el.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
1601
- }
1602
- const parent = el.parentElement;
1603
- if (parent) {
1604
- const siblings = Array.from(parent.children).filter(child => child.nodeName === el.nodeName);
1605
- if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')';
1606
- }
1607
- path.unshift(selector);
1608
- el = parent;
1609
- }
1610
- return path.length ? path.join(' > ') : '';
1611
- }
1612
-
1613
- function textFor(el) {
1614
- return String(
1615
- el.innerText ||
1616
- el.textContent ||
1617
- el.getAttribute('aria-label') ||
1618
- el.getAttribute('placeholder') ||
1619
- el.getAttribute('name') ||
1620
- ''
1621
- ).trim();
1622
- }
1623
-
1624
- // Exact match first
1625
- let best = elements.find(el => textFor(el).toLowerCase() === searchLower);
1626
- if (best) return { selector: cssPath(best), confidence: 1.0 };
1627
-
1628
- // Partial match
1629
- best = elements.find(el => {
1630
- const value = textFor(el).toLowerCase();
1631
- return searchLower.length >= 3 && value.includes(searchLower);
1632
- });
1633
- if (best) return { selector: cssPath(best), confidence: 0.8 };
1634
-
1635
- // Fuzzy match (Levenshtein distance)
1636
- let minDist = Infinity;
1637
- let bestEl = null;
1638
- elements.forEach(el => {
1639
- const elText = textFor(el);
1640
- if (!elText || Math.abs(elText.length - searchText.length) > 8) return;
1641
- const dist = levenshteinDistance(searchText, elText);
1642
- if (dist < minDist && dist <= 3) {
1643
- minDist = dist;
1644
- bestEl = el;
1645
- }
1646
- });
1647
-
1648
- if (bestEl) return { selector: cssPath(bestEl), confidence: 0.6 };
1649
-
1650
- return null;
1651
-
1652
- function levenshteinDistance(a, b) {
1653
- if (a.length === 0) return b.length;
1654
- if (b.length === 0) return a.length;
1655
- const matrix = [];
1656
- for (let i = 0; i <= b.length; i++) matrix[i] = [i];
1657
- for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
1658
- for (let i = 1; i <= b.length; i++) {
1659
- for (let j = 1; j <= a.length; j++) {
1660
- matrix[i][j] = b.charAt(i-1) === a.charAt(j-1) ? matrix[i-1][j-1] :
1661
- Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
1662
- }
1663
- }
1664
- return matrix[b.length][a.length];
1665
- }
1666
- })()
1667
- `);
1668
- if (result) {
1669
- return { selector: result.selector, method: "fuzzy", confidence: result.confidence };
742
+ if (emulation) {
743
+ if (emulation.width && emulation.height) {
744
+ await this.client.sendCommand("Emulation.setDeviceMetricsOverride", {
745
+ width: emulation.width, height: emulation.height,
746
+ deviceScaleFactor: emulation.scale || 1, mobile: emulation.mobile || false,
747
+ });
748
+ results.push(`Emulated device: ${emulation.width}x${emulation.height}`);
749
+ }
750
+ if (emulation.userAgent) {
751
+ await this.client.sendCommand("Emulation.setUserAgentOverride", { userAgent: emulation.userAgent });
752
+ results.push("Set custom user agent");
1670
753
  }
1671
754
  }
1672
- throw new Error(`Could not resolve selector. Original: ${originalSelector}, Text: ${text}`);
755
+ if (script?.onLoad) {
756
+ await this.client.sendCommand("Page.addScriptToEvaluateOnNewDocument", { source: script.onLoad });
757
+ results.push("Added script to run on new documents");
758
+ }
759
+ return { success: true, configured: results, message: results.join(", ") || "No configuration applied" };
1673
760
  }
1674
- async getTabs(params) {
1675
- const result = await this.client.sendCommand("Target.getTargets", {});
1676
- return result.targetInfos || [];
761
+ async emulateNetworkConditions(params) {
762
+ await this.client.sendCommand("Network.emulateNetworkConditions", {
763
+ offline: params.offline || false,
764
+ latency: params.latency || 0,
765
+ downloadThroughput: params.downloadThroughput || 0,
766
+ uploadThroughput: params.uploadThroughput || 0,
767
+ });
768
+ return "Network conditions emulated";
1677
769
  }
1678
- async newTab(params) {
1679
- const result = await this.client.sendCommand("Target.createTarget", {
1680
- url: params.url || "about:blank",
770
+ async setGeolocation(params) {
771
+ await this.client.sendCommand("Emulation.setGeolocationOverride", {
772
+ latitude: params.latitude, longitude: params.longitude, accuracy: params.accuracy || 100,
1681
773
  });
1682
- return result.targetId ? `Created new tab: ${result.targetId}` : "Created new tab";
774
+ return `Geolocation set to ${params.latitude}, ${params.longitude}`;
1683
775
  }
1684
- async switchTab(params) {
1685
- if (!params.targetId)
1686
- throw new Error("targetId required to switch tabs");
1687
- await this.client.sendCommand("Target.activateTarget", { targetId: params.targetId });
1688
- await this.client.switchToTarget(params.targetId, params.port || 9222);
1689
- return `Switched to tab ${params.targetId}`;
776
+ async setTimezone(params) {
777
+ await this.client.sendCommand("Emulation.setTimezoneOverride", { timezoneId: params.timezoneId });
778
+ return `Timezone set to ${params.timezoneId}`;
1690
779
  }
1691
- async closeTab(params) {
1692
- await this.client.sendCommand("Target.closeTarget", {
1693
- targetId: params.targetId,
780
+ async printPDF(params) {
781
+ const result = await this.client.sendCommand("Page.printToPDF", {
782
+ landscape: params.landscape || false,
783
+ printBackground: params.printBackground || false,
784
+ ...params.options,
1694
785
  });
1695
- return "Closed tab";
786
+ return result.data;
1696
787
  }
788
+ async uploadFile(params) {
789
+ const selector = params.selector;
790
+ const files = params.files || [];
791
+ if (!selector)
792
+ throw new Error("Selector required for upload_file");
793
+ if (!files.length)
794
+ throw new Error("No files specified");
795
+ const result = await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(selector)});if(!el)return {success:false,error:"Element not found"};if(el.type!=='file')return {success:false,error:"Element is not a file input"};el.style.display='block';el.click();return {success:true};})()`);
796
+ if (!result?.success)
797
+ throw new Error(result?.error || "Failed to activate file input");
798
+ const docResult = await this.client.sendCommand("DOM.getDocument", {});
799
+ const queryResult = await this.client.sendCommand("DOM.querySelector", {
800
+ nodeId: docResult.root.nodeId, selector,
801
+ });
802
+ if (!queryResult.nodeId)
803
+ throw new Error(`File input not found: ${selector}`);
804
+ await this.client.sendCommand("DOM.setFileInputFiles", { files, nodeId: queryResult.nodeId });
805
+ return "File upload completed";
806
+ }
807
+ // ─── Network Mocking ──────────────────────────────────────────────
808
+ async mockNetworkRequest(params) {
809
+ const urlPattern = params.urlPattern;
810
+ const mockResponse = params.mockResponse;
811
+ if (!urlPattern)
812
+ throw new Error("urlPattern required");
813
+ this.mockRoutes.push({ pattern: urlPattern, response: mockResponse || "{}" });
814
+ await this.client.sendCommand("Fetch.enable", {
815
+ patterns: this.mockRoutes.map((route) => ({ urlPattern: route.pattern })),
816
+ });
817
+ if (!this.mockRouteListener) {
818
+ this.mockRouteListener = async (event) => {
819
+ const route = this.mockRoutes.find((item) => this.matchesUrlPattern(event.request.url, item.pattern));
820
+ if (!route) {
821
+ await this.client.sendCommand("Fetch.continueRequest", { requestId: event.requestId }).catch(() => { });
822
+ return;
823
+ }
824
+ await this.client.sendCommand("Fetch.fulfillRequest", {
825
+ requestId: event.requestId,
826
+ responseCode: 200,
827
+ responseHeaders: [
828
+ { name: "Content-Type", value: "application/json" },
829
+ { name: "Access-Control-Allow-Origin", value: "*" },
830
+ ],
831
+ body: Buffer.from(route.response).toString("base64"),
832
+ }).catch(() => { });
833
+ };
834
+ this.client.on("Fetch.requestPaused", this.mockRouteListener);
835
+ }
836
+ return `Mocking enabled for pattern: ${urlPattern}`;
837
+ }
838
+ matchesUrlPattern(url, pattern) {
839
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
840
+ return new RegExp(`^${escaped}$`).test(url);
841
+ }
842
+ // ─── Screencast ───────────────────────────────────────────────────
1697
843
  async startScreencast(params) {
1698
844
  this.screencastFrames = [];
1699
845
  if (!this.screencastFrameListener) {
@@ -1713,7 +859,7 @@ class CdpBridge {
1713
859
  await this.client.sendCommand("Page.startScreencast", {
1714
860
  format: params.format || "jpeg",
1715
861
  quality: params.quality || 80,
1716
- everyNthFrame: params.everyNthFrame || 1
862
+ everyNthFrame: params.everyNthFrame || 1,
1717
863
  });
1718
864
  return "Started screencast";
1719
865
  }
@@ -1737,14 +883,10 @@ class CdpBridge {
1737
883
  };
1738
884
  this.client.on("Page.screencastFrame", onFrame);
1739
885
  await this.startScreencast(params);
1740
- await new Promise(r => setTimeout(r, duration));
886
+ await new Promise((r) => setTimeout(r, duration));
1741
887
  await this.stopScreencast(params);
1742
888
  this.client.removeEventListener("Page.screencastFrame", onFrame);
1743
- return {
1744
- frames,
1745
- frameCount: frames.length,
1746
- duration
1747
- };
889
+ return { frames, frameCount: frames.length, duration };
1748
890
  }
1749
891
  async sampleVisualFrames(params) {
1750
892
  const duration = Math.max(250, Math.min(Number(params.duration ?? 1500), 10000));
@@ -1764,550 +906,25 @@ class CdpBridge {
1764
906
  quality: Math.max(20, Math.min(Number(params.quality ?? 45), 80)),
1765
907
  maxWidth: Math.max(320, Math.min(Number(params.maxWidth ?? 800), 1280)),
1766
908
  maxHeight: Math.max(240, Math.min(Number(params.maxHeight ?? 600), 900)),
1767
- everyNthFrame: Math.max(1, Math.min(Number(params.everyNthFrame ?? 3), 10))
909
+ everyNthFrame: Math.max(1, Math.min(Number(params.everyNthFrame ?? 3), 10)),
1768
910
  });
1769
911
  try {
1770
- await new Promise(r => setTimeout(r, duration));
912
+ await new Promise((r) => setTimeout(r, duration));
1771
913
  }
1772
914
  finally {
1773
915
  await this.client.sendCommand("Page.stopScreencast", {}).catch(() => { });
1774
916
  this.client.removeEventListener("Page.screencastFrame", onFrame);
1775
917
  }
1776
- return {
1777
- success: true,
1778
- frameCount: frames.length,
1779
- duration,
1780
- format: params.format || "jpeg",
1781
- timestamps,
1782
- frames
1783
- };
1784
- }
1785
- async startTracing(params) {
1786
- await this.client.sendCommand("Tracing.start", { categories: params.categories || "devtools.timeline" });
1787
- return "Started tracing";
1788
- }
1789
- async stopTracing(params) {
1790
- await this.client.sendCommand("Tracing.end", {});
1791
- return "Stopped tracing";
1792
- }
1793
- async getPerformanceMetrics(params) {
1794
- await this.client.sendCommand("Performance.enable", {});
1795
- const result = await this.client.sendCommand("Performance.getMetrics", {});
1796
- return result.metrics;
1797
- }
1798
- async hover(params) {
1799
- const x = params.x || (params.coordinate ? Number(params.coordinate.split(',')[0]) : 100);
1800
- const y = params.y || (params.coordinate ? Number(params.coordinate.split(',')[1]) : 100);
1801
- await this.client.moveMouse(x, y);
1802
- return "Hovered";
1803
- }
1804
- async dragAndDrop(params) {
1805
- const startX = params.startX || 0;
1806
- const startY = params.startY || 0;
1807
- const endX = params.endX || 0;
1808
- const endY = params.endY || 0;
1809
- await this.client.moveMouse(startX, startY);
1810
- await this.client.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x: startX, y: startY, button: "left", clickCount: 1 });
1811
- await this.client.moveMouse(endX, endY);
1812
- await this.client.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x: endX, y: endY, button: "left", clickCount: 1 });
1813
- return "Dragged and dropped";
1814
- }
1815
- // ==================== MISSING ACT TOOL ACTION IMPLEMENTATIONS ====================
1816
- async fillInput(params) {
1817
- const selector = params.selector;
1818
- const text = params.value || params.text || "";
1819
- if (selector) {
1820
- // Wait for element and clear it first
1821
- await this.client.waitForSelector(selector);
1822
- await this.client.moveMouseToSelector(selector).catch(() => { });
1823
- await this.client.evaluate(`
1824
- (function() {
1825
- const el = document.querySelector(${JSON.stringify(selector)});
1826
- if (el) {
1827
- el.value = '';
1828
- el.focus();
1829
- return true;
1830
- }
1831
- return false;
1832
- })()
1833
- `);
1834
- }
1835
- await this.client.typeText(text);
1836
- return `Filled with: ${text}`;
1837
- }
1838
- async selectOption(params) {
1839
- const selector = params.selector;
1840
- const value = params.value || "";
1841
- if (!selector)
1842
- throw new Error("Selector required for select action");
1843
- await this.client.waitForSelector(selector);
1844
- const selectInfo = await this.client.evaluate(`
1845
- (function() {
1846
- const select = document.querySelector(${JSON.stringify(selector)});
1847
- if (!select) return { success: false, error: "Element not found" };
1848
- if (select.tagName.toLowerCase() !== 'select') return { success: false, error: "Element is not a select" };
1849
- if (select.disabled) return { success: false, error: "Element is disabled" };
1850
- const wanted = ${JSON.stringify(value)};
1851
- const options = Array.from(select.options || []);
1852
- const index = options.findIndex((option) =>
1853
- option.value === wanted ||
1854
- option.text === wanted ||
1855
- option.label === wanted
1856
- );
1857
- return {
1858
- success: true,
1859
- selectedValue: select.value,
1860
- index,
1861
- optionCount: options.length,
1862
- wantedValue: index >= 0 ? options[index].value : wanted,
1863
- };
1864
- })()
1865
- `);
1866
- if (!selectInfo?.success)
1867
- throw new Error(selectInfo?.error || "Failed to inspect select");
1868
- if (selectInfo.selectedValue === selectInfo.wantedValue) {
1869
- return `Selected option: ${value}`;
1870
- }
1871
- if (selectInfo.index >= 0 && selectInfo.index <= 40) {
1872
- try {
1873
- await this.clickElementBySelector({ selector });
1874
- await this.client.pressKey("Home");
1875
- for (let i = 0; i < selectInfo.index; i++) {
1876
- await this.client.pressKey("ArrowDown");
1877
- }
1878
- await this.client.pressKey("Enter");
1879
- const verified = await this.client.evaluate(`
1880
- (function() {
1881
- const select = document.querySelector(${JSON.stringify(selector)});
1882
- return select ? select.value : null;
1883
- })()
1884
- `);
1885
- if (verified === selectInfo.wantedValue) {
1886
- return `Selected option: ${value}`;
1887
- }
1888
- }
1889
- catch {
1890
- // Fall back to direct value setting below for reliability.
1891
- }
1892
- }
1893
- const result = await this.client.evaluate(`
1894
- (function() {
1895
- const select = document.querySelector(${JSON.stringify(selector)});
1896
- if (!select) return { success: false, error: "Element not found" };
1897
- select.value = ${JSON.stringify(selectInfo.wantedValue)};
1898
- select.dispatchEvent(new Event('input', { bubbles: true }));
1899
- select.dispatchEvent(new Event('change', { bubbles: true }));
1900
- return { success: true, selectedValue: select.value };
1901
- })()
1902
- `);
1903
- if (result?.success)
1904
- return `Selected option: ${value}`;
1905
- throw new Error(result?.error || "Failed to select option");
1906
- }
1907
- async checkElement(params) {
1908
- const selector = params.selector;
1909
- if (!selector)
1910
- throw new Error("Selector required for check action");
1911
- return this.setChecked({ selector, checked: true });
1912
- }
1913
- async setChecked(params) {
1914
- const selector = params.selector;
1915
- if (!selector)
1916
- throw new Error("Selector required for checked state");
1917
- await this.client.waitForSelector(selector);
1918
- const before = await this.client.evaluate(`
1919
- (function() {
1920
- const el = document.querySelector(${JSON.stringify(selector)});
1921
- if (!el) return { success: false, error: "Element not found" };
1922
- if (el.type !== 'checkbox' && el.type !== 'radio') return { success: false, error: "Element is not a checkbox or radio" };
1923
- if (el.disabled) return { success: false, error: "Element is disabled" };
1924
- return { success: true, checked: !!el.checked, type: el.type };
1925
- })()
1926
- `);
1927
- if (!before?.success)
1928
- throw new Error(before?.error || "Failed to inspect checked state");
1929
- const wanted = !!params.checked;
1930
- if (before.checked === wanted)
1931
- return `Checked state set to ${wanted}`;
1932
- if (!(before.type === "radio" && !wanted)) {
1933
- try {
1934
- await this.clickElementBySelector({ selector });
1935
- const afterClick = await this.client.evaluate(`
1936
- (function() {
1937
- const el = document.querySelector(${JSON.stringify(selector)});
1938
- return el ? !!el.checked : null;
1939
- })()
1940
- `);
1941
- if (afterClick === wanted)
1942
- return `Checked state set to ${wanted}`;
1943
- }
1944
- catch {
1945
- // Fall back to direct state setting below for reliability.
1946
- }
1947
- }
1948
- const result = await this.client.evaluate(`
1949
- (function() {
1950
- const el = document.querySelector(${JSON.stringify(selector)});
1951
- if (!el) return { success: false, error: "Element not found" };
1952
- if (el.type !== 'checkbox' && el.type !== 'radio') return { success: false, error: "Element is not a checkbox or radio" };
1953
- el.checked = ${JSON.stringify(!!params.checked)};
1954
- el.dispatchEvent(new Event('input', { bubbles: true }));
1955
- el.dispatchEvent(new Event('change', { bubbles: true }));
1956
- return { success: true, checked: el.checked };
1957
- })()
1958
- `);
1959
- if (result?.success)
1960
- return `Checked state set to ${!!params.checked}`;
1961
- throw new Error(result?.error || "Failed to set checked state");
1962
- }
1963
- async getAccessibilityTree(params) {
1964
- return await this.client.getSimplifiedAccessibilityTree();
1965
- }
1966
- async getDOMTree(params) {
1967
- const result = await this.client.getDOMSnapshot();
1968
- return result;
1969
- }
1970
- async assertCondition(params) {
1971
- const assertionType = params.assertionType || "element_exists";
1972
- const selector = params.selector;
1973
- const expectedText = params.expectedText || params.value || "";
1974
- const result = await this.client.evaluate(`
1975
- (function() {
1976
- const selector = ${JSON.stringify(selector)};
1977
- const type = ${JSON.stringify(assertionType)};
1978
- const expectedText = ${JSON.stringify(expectedText)};
1979
-
1980
- const el = selector ? document.querySelector(selector) : null;
1981
-
1982
- switch(type) {
1983
- case 'element_exists':
1984
- return { success: !!el, message: el ? 'Element exists' : 'Element not found' };
1985
- case 'element_not_exists':
1986
- return { success: !el, message: !el ? 'Element does not exist' : 'Element found' };
1987
- case 'element_contains_text':
1988
- if (!el) return { success: false, message: 'Element not found' };
1989
- const text = (el.innerText || el.textContent || '').trim();
1990
- const matches = text.includes(expectedText);
1991
- return { success: matches, message: matches ? 'Text matches' : 'Text does not match', actualText: text };
1992
- case 'url_contains':
1993
- const urlMatches = window.location.href.includes(expectedText);
1994
- return { success: urlMatches, message: urlMatches ? 'URL contains text' : 'URL does not contain text' };
1995
- default:
1996
- return { success: false, message: 'Unknown assertion type' };
1997
- }
1998
- })()
1999
- `);
2000
- return result || { success: false, message: "Assertion failed" };
2001
- }
2002
- async getCookies(params) {
2003
- const result = await this.client.sendCommand("Network.getCookies", {
2004
- urls: [await this.client.evaluate("window.location.href").catch(() => "*") || "*"],
2005
- });
2006
- return result.cookies || [];
2007
- }
2008
- async setCookie(params) {
2009
- const cookies = [{
2010
- name: params.cookieName || params.name,
2011
- value: params.cookieValue || params.value,
2012
- url: params.url || await this.client.evaluate("window.location.href").catch(() => undefined),
2013
- domain: params.domain,
2014
- path: params.path || "/",
2015
- secure: params.secure || false,
2016
- httpOnly: params.httpOnly || false,
2017
- }];
2018
- await this.client.sendCommand("Network.setCookies", { cookies });
2019
- return "Cookie set";
2020
- }
2021
- async clearCache(params) {
2022
- await this.client.sendCommand("Network.clearBrowserCache", {});
2023
- await this.client.sendCommand("Network.clearBrowserCookies", {});
2024
- return "Cache cleared";
2025
- }
2026
- async setGeolocation(params) {
2027
- await this.client.sendCommand("Emulation.setGeolocationOverride", {
2028
- latitude: params.latitude,
2029
- longitude: params.longitude,
2030
- accuracy: params.accuracy || 100,
2031
- });
2032
- return `Geolocation set to ${params.latitude}, ${params.longitude}`;
2033
- }
2034
- async setTimezone(params) {
2035
- await this.client.sendCommand("Emulation.setTimezoneOverride", {
2036
- timezoneId: params.timezoneId,
2037
- });
2038
- return `Timezone set to ${params.timezoneId}`;
2039
- }
2040
- async emulateNetworkConditions(params) {
2041
- await this.client.sendCommand("Network.emulateNetworkConditions", {
2042
- offline: params.offline || false,
2043
- latency: params.latency || 0,
2044
- downloadThroughput: params.downloadThroughput || 0,
2045
- uploadThroughput: params.uploadThroughput || 0,
2046
- });
2047
- return "Network conditions emulated";
2048
- }
2049
- async printPDF(params) {
2050
- const result = await this.client.sendCommand("Page.printToPDF", {
2051
- landscape: params.landscape || false,
2052
- printBackground: params.printBackground || false,
2053
- ...params.options,
2054
- });
2055
- return result.data; // base64 PDF
2056
- }
2057
- async highlightElements(params) {
2058
- const result = await this.client.getInteractiveElements(true);
2059
- return {
2060
- success: true,
2061
- elements: result.elements,
2062
- message: `Highlighted ${result.elements.length} elements`,
2063
- };
2064
- }
2065
- async verifyUIState(params) {
2066
- const selector = params.selector;
2067
- const expectedText = params.expectedText || params.value || "";
2068
- const result = await this.client.evaluate(`
2069
- (function() {
2070
- const el = document.querySelector(${JSON.stringify(selector)});
2071
- if (!el) return { exists: false, visible: false };
2072
-
2073
- const rect = el.getBoundingClientRect();
2074
- const computed = window.getComputedStyle(el);
2075
- const visible = rect.width > 0 && rect.height > 0 &&
2076
- computed.display !== 'none' &&
2077
- computed.visibility !== 'hidden' &&
2078
- computed.opacity !== '0';
2079
-
2080
- return {
2081
- exists: true,
2082
- visible,
2083
- text: (el.innerText || el.textContent || '').trim().substring(0, 200),
2084
- bounds: { x: rect.left, y: rect.top, width: rect.width, height: rect.height }
2085
- };
2086
- })()
2087
- `);
2088
- return result || { exists: false, visible: false };
2089
- }
2090
- async getDOMStorage(params) {
2091
- await this.client.sendCommand("DOMStorage.enable", {});
2092
- const origin = params.origin || await this.client.evaluate("window.location.origin").catch(() => "");
2093
- const result = await this.client.sendCommand("DOMStorage.getDOMStorageItems", {
2094
- storageId: { securityOrigin: origin, isLocalStorage: params.type !== 'session' },
2095
- });
2096
- return result.entries || [];
918
+ return { success: true, frameCount: frames.length, duration, format: params.format || "jpeg", timestamps, frames };
2097
919
  }
2098
- async getNetworkTraffic(params) {
2099
- return await this.client.getNetworkTraffic();
2100
- }
2101
- async getNetworkResponse(params) {
2102
- const requestId = params.requestId;
2103
- if (!requestId)
2104
- throw new Error("requestId required");
2105
- const result = await this.client.sendCommand("Network.getResponseBody", { requestId });
2106
- return result;
2107
- }
2108
- async mockNetworkRequest(params) {
2109
- const urlPattern = params.urlPattern;
2110
- const mockResponse = params.mockResponse;
2111
- if (!urlPattern)
2112
- throw new Error("urlPattern required");
2113
- this.mockRoutes.push({
2114
- pattern: urlPattern,
2115
- response: mockResponse || "{}"
2116
- });
2117
- await this.client.sendCommand("Fetch.enable", {
2118
- patterns: this.mockRoutes.map(route => ({ urlPattern: route.pattern }))
2119
- });
2120
- if (!this.mockRouteListener) {
2121
- this.mockRouteListener = async (event) => {
2122
- const route = this.mockRoutes.find(item => this.matchesUrlPattern(event.request.url, item.pattern));
2123
- if (!route) {
2124
- await this.client.sendCommand("Fetch.continueRequest", { requestId: event.requestId }).catch(() => { });
2125
- return;
2126
- }
2127
- await this.client.sendCommand("Fetch.fulfillRequest", {
2128
- requestId: event.requestId,
2129
- responseCode: 200,
2130
- responseHeaders: [
2131
- { name: "Content-Type", value: "application/json" },
2132
- { name: "Access-Control-Allow-Origin", value: "*" }
2133
- ],
2134
- body: Buffer.from(route.response).toString("base64")
2135
- }).catch(() => { });
2136
- };
2137
- this.client.on("Fetch.requestPaused", this.mockRouteListener);
2138
- }
2139
- return `Mocking enabled for pattern: ${urlPattern}`;
2140
- }
2141
- matchesUrlPattern(url, pattern) {
2142
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2143
- return new RegExp(`^${escaped}$`).test(url);
2144
- }
2145
- async getComputedStyle(params) {
2146
- const selector = params.selector;
2147
- const property = params.property;
2148
- if (!selector)
2149
- throw new Error("Selector required");
2150
- const result = await this.client.evaluate(`
2151
- (function() {
2152
- const el = document.querySelector(${JSON.stringify(selector)});
2153
- if (!el) return null;
2154
- const style = window.getComputedStyle(el);
2155
- ${property ? `return style.getPropertyValue(${JSON.stringify(property)});` : `return JSON.parse(JSON.stringify(style));`}
2156
- })()
2157
- `);
2158
- return result;
2159
- }
2160
- async getEventListeners(params) {
2161
- const selector = params.selector;
2162
- if (!selector)
2163
- throw new Error("Selector required");
2164
- const result = await this.client.evaluate(`
2165
- (function() {
2166
- const el = document.querySelector(${JSON.stringify(selector)});
2167
- if (!el) return null;
2168
- // getEventListeners is Chrome DevTools specific, not available in page context
2169
- return { message: "getEventListeners requires DevTools protocol, not available in page context" };
2170
- })()
2171
- `);
2172
- return result;
2173
- }
2174
- screencastFrames = [];
2175
- screencastFrameListener = null;
2176
- mockRoutes = [];
2177
- mockRouteListener = null;
2178
920
  async getScreencastFrames(params) {
2179
921
  const maxFrames = params.maxFrames || this.screencastFrames.length;
2180
922
  const frames = this.screencastFrames.slice(-maxFrames);
2181
- return {
2182
- frameCount: frames.length,
2183
- frames,
2184
- message: `Retrieved ${frames.length} frames`
2185
- };
2186
- }
2187
- async uploadFile(params) {
2188
- const selector = params.selector;
2189
- const files = params.files || [];
2190
- if (!selector)
2191
- throw new Error("Selector required for upload_file action");
2192
- if (!files.length)
2193
- throw new Error("No files specified");
2194
- // Click the file input to activate it
2195
- const result = await this.client.evaluate(`
2196
- (function() {
2197
- const el = document.querySelector(${JSON.stringify(selector)});
2198
- if (!el) return { success: false, error: "Element not found" };
2199
- if (el.type !== 'file') return { success: false, error: "Element is not a file input" };
2200
- el.style.display = 'block'; // Make sure it's visible
2201
- el.click();
2202
- return { success: true };
2203
- })()
2204
- `);
2205
- if (!result?.success) {
2206
- throw new Error(result?.error || "Failed to activate file input");
2207
- }
2208
- const documentResult = await this.client.sendCommand("DOM.getDocument", {});
2209
- const queryResult = await this.client.sendCommand("DOM.querySelector", {
2210
- nodeId: documentResult.root.nodeId,
2211
- selector
2212
- });
2213
- if (!queryResult.nodeId) {
2214
- throw new Error(`File input not found: ${selector}`);
2215
- }
2216
- await this.client.sendCommand("DOM.setFileInputFiles", {
2217
- files,
2218
- nodeId: queryResult.nodeId
2219
- });
2220
- return "File upload completed";
2221
- }
2222
- async configureBrowser(params) {
2223
- const { network, emulation, script } = params;
2224
- const results = [];
2225
- // Configure network settings
2226
- if (network) {
2227
- if (network.blockImages) {
2228
- await this.client.sendCommand("Network.setBlockedURLs", {
2229
- urls: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.webp", "*.svg"]
2230
- });
2231
- results.push("Blocked images");
2232
- }
2233
- if (network.blockCSS) {
2234
- await this.client.sendCommand("Network.setBlockedURLs", {
2235
- urls: ["*.css"]
2236
- });
2237
- results.push("Blocked CSS");
2238
- }
2239
- if (network.blockAds) {
2240
- await this.client.sendCommand("Network.setBlockedURLs", {
2241
- urls: ["*doubleclick.net*", "*googlesyndication.com*", "*adservice.*"]
2242
- });
2243
- results.push("Blocked ads");
2244
- }
2245
- }
2246
- // Configure emulation settings
2247
- if (emulation) {
2248
- if (emulation.width && emulation.height) {
2249
- await this.client.sendCommand("Emulation.setDeviceMetricsOverride", {
2250
- width: emulation.width,
2251
- height: emulation.height,
2252
- deviceScaleFactor: emulation.scale || 1,
2253
- mobile: emulation.mobile || false,
2254
- });
2255
- results.push(`Emulated device: ${emulation.width}x${emulation.height}`);
2256
- }
2257
- if (emulation.userAgent) {
2258
- await this.client.sendCommand("Emulation.setUserAgentOverride", {
2259
- userAgent: emulation.userAgent,
2260
- });
2261
- results.push("Set custom user agent");
2262
- }
2263
- }
2264
- // Configure scripts
2265
- if (script?.onLoad) {
2266
- await this.client.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
2267
- source: script.onLoad,
2268
- });
2269
- results.push("Added script to run on new documents");
2270
- }
2271
- return {
2272
- success: true,
2273
- configured: results,
2274
- message: results.join(", ") || "No configuration applied"
2275
- };
2276
- }
2277
- async launchBrowser(options) {
2278
- const client = (0, cdp_client_1.getCdpClient)();
2279
- await client.launchAuto({
2280
- browser: options?.browser,
2281
- headless: options?.headless,
2282
- port: options?.port,
2283
- profile: options?.profile,
2284
- profileDirectory: options?.profileDirectory,
2285
- userDataDir: options?.userDataDir,
2286
- });
2287
- const profileLabel = options?.profile || options?.profileDirectory;
2288
- return profileLabel
2289
- ? `Browser launched successfully with profile "${profileLabel}"`
2290
- : "Browser launched successfully";
2291
- }
2292
- async killBrowser() {
2293
- const client = (0, cdp_client_1.getCdpClient)();
2294
- await client.killBrowser();
2295
- return "Browser killed";
2296
- }
2297
- async listBrowsers() {
2298
- const client = (0, cdp_client_1.getCdpClient)();
2299
- return await client.listAvailableBrowsers();
2300
- }
2301
- async listBrowserProfiles(browser) {
2302
- const client = (0, cdp_client_1.getCdpClient)();
2303
- return await client.listBrowserProfiles(browser || "brave");
2304
- }
2305
- async getTargetsViaHttp() {
2306
- // This is a simplified version - in reality we'd need to know the port
2307
- return [];
923
+ return { frameCount: frames.length, frames, message: `Retrieved ${frames.length} frames` };
2308
924
  }
2309
925
  }
2310
926
  exports.CdpBridge = CdpBridge;
927
+ // Singleton
2311
928
  let bridgeInstance = null;
2312
929
  function getCdpBridge() {
2313
930
  if (!bridgeInstance) {