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