ornold-mcp 1.0.0 → 1.0.2

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.
package/dist/cli.js ADDED
@@ -0,0 +1,2788 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/executor/concurrency.ts
13
+ import * as os from "os";
14
+ function detectOptimalConcurrency(browserCount = 0) {
15
+ const cpuCores = os.cpus().length;
16
+ const totalMemGB = os.totalmem() / 1024 ** 3;
17
+ const freeMemGB = os.freemem() / 1024 ** 3;
18
+ const memPerHeavyOp = 0.5;
19
+ const memPerLightOp = 0.2;
20
+ const ramBasedHeavy = Math.floor(freeMemGB / memPerHeavyOp);
21
+ const ramBasedLight = Math.floor(freeMemGB / memPerLightOp);
22
+ const cpuBasedDefault = Math.max(2, Math.floor(cpuCores / 2));
23
+ const cpuBasedLight = Math.max(3, cpuCores);
24
+ const heavyConcurrency = Math.max(2, Math.min(ramBasedHeavy, cpuBasedDefault));
25
+ const lightConcurrency = Math.max(3, Math.min(ramBasedLight, cpuBasedLight));
26
+ const defaultConcurrency = Math.max(2, Math.floor((heavyConcurrency + lightConcurrency) / 2));
27
+ const effectiveHeavy = browserCount > 0 ? Math.min(heavyConcurrency, browserCount) : heavyConcurrency;
28
+ const effectiveLight = browserCount > 0 ? Math.min(lightConcurrency, browserCount) : lightConcurrency;
29
+ const effectiveDefault = browserCount > 0 ? Math.min(defaultConcurrency, browserCount) : defaultConcurrency;
30
+ console.error(`[Concurrency] System: ${cpuCores} cores, ${totalMemGB.toFixed(1)}GB total RAM, ${freeMemGB.toFixed(1)}GB free`);
31
+ console.error(`[Concurrency] Detected limits: default=${effectiveDefault}, heavy=${effectiveHeavy}, light=${effectiveLight}`);
32
+ return {
33
+ default: effectiveDefault,
34
+ heavy: effectiveHeavy,
35
+ light: effectiveLight
36
+ };
37
+ }
38
+ var ConcurrencyLimiter;
39
+ var init_concurrency = __esm({
40
+ "src/executor/concurrency.ts"() {
41
+ "use strict";
42
+ ConcurrencyLimiter = class {
43
+ constructor(maxConcurrent) {
44
+ this.maxConcurrent = maxConcurrent;
45
+ if (maxConcurrent < 1) {
46
+ throw new Error("maxConcurrent must be at least 1");
47
+ }
48
+ }
49
+ maxConcurrent;
50
+ running = 0;
51
+ queue = [];
52
+ get limit() {
53
+ return this.maxConcurrent;
54
+ }
55
+ get active() {
56
+ return this.running;
57
+ }
58
+ get pending() {
59
+ return this.queue.length;
60
+ }
61
+ async acquire() {
62
+ if (this.running < this.maxConcurrent) {
63
+ this.running++;
64
+ return;
65
+ }
66
+ return new Promise((resolve) => {
67
+ this.queue.push(() => {
68
+ this.running++;
69
+ resolve();
70
+ });
71
+ });
72
+ }
73
+ release() {
74
+ if (this.running <= 0) {
75
+ console.error("[ConcurrencyLimiter] Warning: release() called when running <= 0");
76
+ return;
77
+ }
78
+ this.running--;
79
+ const next = this.queue.shift();
80
+ if (next) next();
81
+ }
82
+ async run(fn) {
83
+ await this.acquire();
84
+ try {
85
+ return await fn();
86
+ } finally {
87
+ this.release();
88
+ }
89
+ }
90
+ };
91
+ }
92
+ });
93
+
94
+ // src/executor/human-like.ts
95
+ import * as fs from "fs";
96
+ import * as path from "path";
97
+ function debugLog(category, message, data) {
98
+ try {
99
+ const dir = path.dirname(LOG_FILE);
100
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
101
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
102
+ let line = `[${ts}] [${category}] ${message}`;
103
+ if (data !== void 0) {
104
+ line += ` | ${JSON.stringify(data)}`;
105
+ }
106
+ fs.appendFileSync(LOG_FILE, line + "\n");
107
+ } catch {
108
+ }
109
+ }
110
+ function gaussianRandom(mean, stddev) {
111
+ const u1 = Math.random();
112
+ const u2 = Math.random();
113
+ const z2 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
114
+ return mean + z2 * stddev;
115
+ }
116
+ function clampedRandom(min, max) {
117
+ return Math.min(max, Math.max(min, min + Math.random() * (max - min)));
118
+ }
119
+ function clampedGaussian(mean, stddev, min, max) {
120
+ return Math.min(max, Math.max(min, gaussianRandom(mean, stddev)));
121
+ }
122
+ function sleep(ms) {
123
+ return new Promise((resolve) => setTimeout(resolve, ms));
124
+ }
125
+ function generateProfile(browserId) {
126
+ let hash = 0;
127
+ for (let i = 0; i < browserId.length; i++) {
128
+ hash = (hash << 5) - hash + browserId.charCodeAt(i) | 0;
129
+ }
130
+ const seed = Math.abs(hash);
131
+ return {
132
+ typingSpeed: 0.8 + seed % 100 / 100 * 0.4,
133
+ // 0.8 - 1.2
134
+ mouseSpeed: 0.85 + (seed >> 8) % 100 / 100 * 0.3,
135
+ // 0.85 - 1.15
136
+ actionDelay: 0.7 + (seed >> 16) % 100 / 100 * 0.6
137
+ // 0.7 - 1.3
138
+ };
139
+ }
140
+ async function preActionDelay(profile) {
141
+ const base = clampedGaussian(250, 80, 100, 400);
142
+ const ms = profile ? base * profile.actionDelay : base;
143
+ await sleep(ms);
144
+ }
145
+ function typingDelay(profile) {
146
+ if (Math.random() < 0.05) {
147
+ const pause = clampedGaussian(350, 100, 200, 600);
148
+ return profile ? pause * profile.typingSpeed : pause;
149
+ }
150
+ const base = clampedGaussian(80, 30, 40, 180);
151
+ return profile ? base * profile.typingSpeed : base;
152
+ }
153
+ async function humanDelay(min, max) {
154
+ await sleep(clampedRandom(min, max));
155
+ }
156
+ function cubicBezier(t, p0, p1, p2, p3) {
157
+ const t2 = t * t;
158
+ const t3 = t2 * t;
159
+ const mt = 1 - t;
160
+ const mt2 = mt * mt;
161
+ const mt3 = mt2 * mt;
162
+ return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
163
+ }
164
+ function generateBezierPath(from, to, steps = 20) {
165
+ const dx = to.x - from.x;
166
+ const dy = to.y - from.y;
167
+ const distance = Math.sqrt(dx * dx + dy * dy);
168
+ const spreadX = distance * 0.15;
169
+ const spreadY = distance * 0.15;
170
+ const cp1 = {
171
+ x: from.x + dx * 0.25 + (Math.random() - 0.5) * spreadX,
172
+ y: from.y + dy * 0.25 + (Math.random() - 0.5) * spreadY
173
+ };
174
+ const cp2 = {
175
+ x: from.x + dx * 0.75 + (Math.random() - 0.5) * spreadX,
176
+ y: from.y + dy * 0.75 + (Math.random() - 0.5) * spreadY
177
+ };
178
+ const actualSteps = Math.max(10, Math.min(40, Math.round(steps * (distance / 500))));
179
+ const points = [];
180
+ for (let i = 0; i <= actualSteps; i++) {
181
+ const t = i / actualSteps;
182
+ const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
183
+ points.push({
184
+ x: Math.round(cubicBezier(eased, from.x, cp1.x, cp2.x, to.x)),
185
+ y: Math.round(cubicBezier(eased, from.y, cp1.y, cp2.y, to.y))
186
+ });
187
+ }
188
+ return points;
189
+ }
190
+ async function humanMouseMove(page, from, to, profile) {
191
+ debugLog("MOUSE_MOVE", `from=(${from.x},${from.y}) to=(${to.x},${to.y})`);
192
+ const path2 = generateBezierPath(from, to);
193
+ const speedMult = profile?.mouseSpeed ?? 1;
194
+ debugLog("MOUSE_MOVE", `bezier path generated: ${path2.length} steps, speedMult=${speedMult.toFixed(2)}`);
195
+ let cdpSession = null;
196
+ try {
197
+ cdpSession = await page.context().newCDPSession(page);
198
+ } catch (err) {
199
+ debugLog("MOUSE_MOVE", `CDP session creation failed, cursor won't be visible in screencast`, String(err));
200
+ }
201
+ for (let i = 0; i < path2.length; i++) {
202
+ const point = path2[i];
203
+ const delay = clampedRandom(5, 15) * speedMult;
204
+ try {
205
+ await page.mouse.move(point.x, point.y);
206
+ } catch (err) {
207
+ debugLog("MOUSE_MOVE", `ERROR at step ${i}/${path2.length}: page.mouse.move(${point.x},${point.y}) failed`, String(err));
208
+ throw err;
209
+ }
210
+ if (cdpSession) {
211
+ try {
212
+ await cdpSession.send("Runtime.evaluate", {
213
+ expression: `window.__cpSrv = {x:${Math.round(point.x)},y:${Math.round(point.y)}}`,
214
+ returnByValue: false
215
+ // no need to read back on every step
216
+ });
217
+ } catch (err) {
218
+ }
219
+ }
220
+ await sleep(delay);
221
+ }
222
+ if (cdpSession) {
223
+ try {
224
+ await cdpSession.detach();
225
+ } catch (err) {
226
+ }
227
+ }
228
+ debugLog("MOUSE_MOVE", `movement complete at (${to.x},${to.y})`);
229
+ console.log(`[HumanLike] Mouse moved smoothly to (${to.x},${to.y}), ${path2.length} steps`);
230
+ await showCursorOverlay(page, to.x, to.y);
231
+ }
232
+ function clickOffset(width, height) {
233
+ const maxOffX = Math.min(8, width * 0.15);
234
+ const maxOffY = Math.min(5, height * 0.15);
235
+ return {
236
+ x: clampedGaussian(0, maxOffX * 0.5, -maxOffX, maxOffX),
237
+ y: clampedGaussian(0, maxOffY * 0.5, -maxOffY, maxOffY)
238
+ };
239
+ }
240
+ async function humanClick(page, target, fromPos, profile) {
241
+ debugLog("CLICK", `target=(${target.x},${target.y}) from=(${fromPos.x},${fromPos.y})`);
242
+ await humanMouseMove(page, fromPos, target, profile);
243
+ debugLog("CLICK", `mouse.down()`);
244
+ await page.mouse.down();
245
+ const holdDelay = clampedRandom(50, 120);
246
+ debugLog("CLICK", `holding for ${holdDelay.toFixed(0)}ms`);
247
+ await sleep(holdDelay);
248
+ debugLog("CLICK", `mouse.up()`);
249
+ await page.mouse.up();
250
+ debugLog("CLICK", `click complete`);
251
+ }
252
+ async function getClickTarget(page, locator) {
253
+ debugLog("GET_TARGET", `getting bounding box for locator`);
254
+ const box = await locator.boundingBox();
255
+ if (!box) {
256
+ debugLog("GET_TARGET", `ERROR: no bounding box returned (element not visible)`);
257
+ throw new Error("Element is not visible \u2014 no bounding box");
258
+ }
259
+ debugLog("GET_TARGET", `boundingBox: x=${box.x} y=${box.y} w=${box.width} h=${box.height}`);
260
+ const offset = clickOffset(box.width, box.height);
261
+ const target = {
262
+ x: Math.round(box.x + box.width / 2 + offset.x),
263
+ y: Math.round(box.y + box.height / 2 + offset.y)
264
+ };
265
+ debugLog("GET_TARGET", `center=(${Math.round(box.x + box.width / 2)},${Math.round(box.y + box.height / 2)}) offset=(${offset.x.toFixed(1)},${offset.y.toFixed(1)}) final=(${target.x},${target.y})`);
266
+ return {
267
+ target,
268
+ box: { width: box.width, height: box.height }
269
+ };
270
+ }
271
+ async function cdpTypeChar(cdp, char) {
272
+ const info = KEY_CODES[char];
273
+ const code = info?.code ?? "";
274
+ const keyCode = info?.keyCode ?? char.charCodeAt(0);
275
+ debugLog("TYPE_CHAR", `char="${char}" code=${code} keyCode=${keyCode} (has mapping: ${!!info})`);
276
+ try {
277
+ await cdp.send("Input.dispatchKeyEvent", {
278
+ type: "keyDown",
279
+ key: char,
280
+ code,
281
+ windowsVirtualKeyCode: keyCode,
282
+ nativeVirtualKeyCode: keyCode
283
+ });
284
+ debugLog("TYPE_CHAR", `keyDown sent OK`);
285
+ } catch (err) {
286
+ debugLog("TYPE_CHAR", `keyDown FAILED`, String(err));
287
+ throw err;
288
+ }
289
+ try {
290
+ await cdp.send("Input.dispatchKeyEvent", {
291
+ type: "char",
292
+ text: char,
293
+ unmodifiedText: char
294
+ });
295
+ debugLog("TYPE_CHAR", `char event sent OK`);
296
+ } catch (err) {
297
+ debugLog("TYPE_CHAR", `char event FAILED`, String(err));
298
+ throw err;
299
+ }
300
+ const pressDelay = clampedRandom(20, 60);
301
+ await sleep(pressDelay);
302
+ try {
303
+ await cdp.send("Input.dispatchKeyEvent", {
304
+ type: "keyUp",
305
+ key: char,
306
+ code,
307
+ windowsVirtualKeyCode: keyCode,
308
+ nativeVirtualKeyCode: keyCode
309
+ });
310
+ debugLog("TYPE_CHAR", `keyUp sent OK (pressDelay=${pressDelay.toFixed(0)}ms)`);
311
+ } catch (err) {
312
+ debugLog("TYPE_CHAR", `keyUp FAILED`, String(err));
313
+ throw err;
314
+ }
315
+ }
316
+ async function humanTypeText(page, text, profile) {
317
+ debugLog("TYPE_TEXT", `typing "${text}" (${text.length} chars)`);
318
+ let cdp;
319
+ try {
320
+ cdp = await page.context().newCDPSession(page);
321
+ debugLog("TYPE_TEXT", `CDP session created for typing`);
322
+ } catch (err) {
323
+ debugLog("TYPE_TEXT", `FAILED to create CDP session`, String(err));
324
+ throw err;
325
+ }
326
+ try {
327
+ for (let i = 0; i < text.length; i++) {
328
+ const char = text[i];
329
+ await cdpTypeChar(cdp, char);
330
+ const delay = typingDelay(profile);
331
+ debugLog("TYPE_TEXT", `char ${i + 1}/${text.length} "${char}" typed, next delay=${delay.toFixed(0)}ms`);
332
+ await sleep(delay);
333
+ }
334
+ debugLog("TYPE_TEXT", `typing complete`);
335
+ } finally {
336
+ await cdp.detach().catch(() => {
337
+ });
338
+ debugLog("TYPE_TEXT", `CDP session detached`);
339
+ }
340
+ }
341
+ async function getOverlaySession(page) {
342
+ let cdp = overlaySessions.get(page);
343
+ if (cdp) {
344
+ debugLog("OVERLAY", `reusing existing overlay session`);
345
+ return cdp;
346
+ }
347
+ debugLog("OVERLAY", `creating new overlay CDP session`);
348
+ cdp = await page.context().newCDPSession(page);
349
+ try {
350
+ await cdp.send("DOM.enable");
351
+ debugLog("OVERLAY", `DOM.enable SUCCESS`);
352
+ await cdp.send("Overlay.enable");
353
+ debugLog("OVERLAY", `Overlay.enable SUCCESS`);
354
+ } catch (err) {
355
+ debugLog("OVERLAY", `Overlay.enable FAILED`, String(err));
356
+ }
357
+ overlaySessions.set(page, cdp);
358
+ cdp.on("detached", () => {
359
+ debugLog("OVERLAY", `overlay session detached (cleanup)`);
360
+ overlaySessions.delete(page);
361
+ });
362
+ return cdp;
363
+ }
364
+ async function showCursorOverlay(page, x, y) {
365
+ debugLog("CURSOR", `showCursorOverlay called at (${x},${y})`);
366
+ try {
367
+ const cdp = await getOverlaySession(page);
368
+ const params = {
369
+ x: Math.round(x) - 4,
370
+ y: Math.round(y) - 4,
371
+ width: 8,
372
+ height: 8,
373
+ color: { r: 255, g: 69, b: 58, a: 0.7 },
374
+ outlineColor: { r: 255, g: 255, b: 255, a: 0.9 }
375
+ };
376
+ debugLog("CURSOR", `sending Overlay.highlightRect`, params);
377
+ await cdp.send("Overlay.highlightRect", params);
378
+ debugLog("CURSOR", `highlightRect SUCCESS`);
379
+ } catch (err) {
380
+ debugLog("CURSOR", `highlightRect FAILED (attempt 1), retrying...`, String(err));
381
+ overlaySessions.delete(page);
382
+ try {
383
+ const cdp = await getOverlaySession(page);
384
+ await cdp.send("Overlay.highlightRect", {
385
+ x: Math.round(x) - 4,
386
+ y: Math.round(y) - 4,
387
+ width: 8,
388
+ height: 8,
389
+ color: { r: 255, g: 69, b: 58, a: 0.7 },
390
+ outlineColor: { r: 255, g: 255, b: 255, a: 0.9 }
391
+ });
392
+ debugLog("CURSOR", `highlightRect SUCCESS (retry)`);
393
+ } catch (err2) {
394
+ debugLog("CURSOR", `highlightRect FAILED (retry), giving up`, String(err2));
395
+ }
396
+ }
397
+ }
398
+ async function ensureCursor(page) {
399
+ }
400
+ var LOG_FILE, KEY_CODES, SYMBOLS, overlaySessions;
401
+ var init_human_like = __esm({
402
+ "src/executor/human-like.ts"() {
403
+ "use strict";
404
+ LOG_FILE = path.join(process.cwd(), "logs", "human-like-debug.log");
405
+ KEY_CODES = {};
406
+ for (let i = 0; i < 26; i++) {
407
+ const lower = String.fromCharCode(97 + i);
408
+ const upper = String.fromCharCode(65 + i);
409
+ KEY_CODES[lower] = { keyCode: 65 + i, code: `Key${upper}`, key: lower };
410
+ KEY_CODES[upper] = { keyCode: 65 + i, code: `Key${upper}`, key: upper };
411
+ }
412
+ for (let i = 0; i < 10; i++) {
413
+ const ch = String(i);
414
+ KEY_CODES[ch] = { keyCode: 48 + i, code: `Digit${i}`, key: ch };
415
+ }
416
+ SYMBOLS = {
417
+ " ": { keyCode: 32, code: "Space" },
418
+ ".": { keyCode: 190, code: "Period" },
419
+ ",": { keyCode: 188, code: "Comma" },
420
+ "/": { keyCode: 191, code: "Slash" },
421
+ "\\": { keyCode: 220, code: "Backslash" },
422
+ "-": { keyCode: 189, code: "Minus" },
423
+ "=": { keyCode: 187, code: "Equal" },
424
+ "[": { keyCode: 219, code: "BracketLeft" },
425
+ "]": { keyCode: 221, code: "BracketRight" },
426
+ ";": { keyCode: 186, code: "Semicolon" },
427
+ "'": { keyCode: 222, code: "Quote" },
428
+ "`": { keyCode: 192, code: "Backquote" },
429
+ "@": { keyCode: 50, code: "Digit2" },
430
+ "!": { keyCode: 49, code: "Digit1" },
431
+ "#": { keyCode: 51, code: "Digit3" },
432
+ "$": { keyCode: 52, code: "Digit4" },
433
+ "%": { keyCode: 53, code: "Digit5" },
434
+ "^": { keyCode: 54, code: "Digit6" },
435
+ "&": { keyCode: 55, code: "Digit7" },
436
+ "*": { keyCode: 56, code: "Digit8" },
437
+ "(": { keyCode: 57, code: "Digit9" },
438
+ ")": { keyCode: 48, code: "Digit0" },
439
+ "_": { keyCode: 189, code: "Minus" },
440
+ "+": { keyCode: 187, code: "Equal" },
441
+ "{": { keyCode: 219, code: "BracketLeft" },
442
+ "}": { keyCode: 221, code: "BracketRight" },
443
+ ":": { keyCode: 186, code: "Semicolon" },
444
+ '"': { keyCode: 222, code: "Quote" },
445
+ "~": { keyCode: 192, code: "Backquote" },
446
+ "<": { keyCode: 188, code: "Comma" },
447
+ ">": { keyCode: 190, code: "Period" },
448
+ "?": { keyCode: 191, code: "Slash" },
449
+ "|": { keyCode: 220, code: "Backslash" },
450
+ "\n": { keyCode: 13, code: "Enter" },
451
+ " ": { keyCode: 9, code: "Tab" }
452
+ };
453
+ for (const [ch, info] of Object.entries(SYMBOLS)) {
454
+ KEY_CODES[ch] = { ...info, key: ch };
455
+ }
456
+ overlaySessions = /* @__PURE__ */ new WeakMap();
457
+ }
458
+ });
459
+
460
+ // src/executor/multi-browser.ts
461
+ var multi_browser_exports = {};
462
+ __export(multi_browser_exports, {
463
+ MultiBrowserExecutor: () => MultiBrowserExecutor
464
+ });
465
+ import * as playwright from "patchright";
466
+ import { readFile, readdir, stat } from "node:fs/promises";
467
+ import { join as join2, extname } from "node:path";
468
+ var MultiBrowserExecutor;
469
+ var init_multi_browser = __esm({
470
+ "src/executor/multi-browser.ts"() {
471
+ "use strict";
472
+ init_concurrency();
473
+ init_human_like();
474
+ MultiBrowserExecutor = class _MultiBrowserExecutor {
475
+ browsers = /* @__PURE__ */ new Map();
476
+ defaultTimeout;
477
+ limiter;
478
+ heavyLimiter;
479
+ lightLimiter;
480
+ // Cache for browser sync - prevents excessive API calls
481
+ lastSyncTime = 0;
482
+ syncInProgress = null;
483
+ static SYNC_TTL_MS = 5e3;
484
+ // 5 seconds cache TTL
485
+ // Track last mouse position per browser for human-like movement
486
+ lastMousePosition = /* @__PURE__ */ new Map();
487
+ // Per-browser human profiles for varied timing
488
+ profiles = /* @__PURE__ */ new Map();
489
+ // Snapshot refs map back to selectors for flow-agent style browser tools
490
+ snapshotRefs = /* @__PURE__ */ new Map();
491
+ // Console messages collected per browser (since connect time)
492
+ consoleMessages = /* @__PURE__ */ new Map();
493
+ // Network requests collected per browser (since connect time)
494
+ networkRequests = /* @__PURE__ */ new Map();
495
+ constructor(options = {}) {
496
+ this.defaultTimeout = options.timeout ?? 5e3;
497
+ const detected = detectOptimalConcurrency();
498
+ const defaultConcurrency = options.maxConcurrency ?? detected.default;
499
+ const heavyConcurrency = options.maxConcurrency ? Math.max(2, Math.floor(options.maxConcurrency * 0.75)) : detected.heavy;
500
+ const lightConcurrency = options.maxConcurrency ? Math.min(options.maxConcurrency * 1.5, 10) : detected.light;
501
+ this.limiter = new ConcurrencyLimiter(defaultConcurrency);
502
+ this.heavyLimiter = new ConcurrencyLimiter(heavyConcurrency);
503
+ this.lightLimiter = new ConcurrencyLimiter(lightConcurrency);
504
+ console.error(`[Executor] Concurrency limiters initialized: default=${defaultConcurrency}, heavy=${heavyConcurrency}, light=${lightConcurrency}`);
505
+ }
506
+ // ==================== CONNECTION MANAGEMENT ====================
507
+ /**
508
+ * Connect to multiple browsers with controlled concurrency.
509
+ * OPTIMIZATION: Limits simultaneous connections to prevent overwhelming CDP endpoints.
510
+ */
511
+ async connectAll(endpoints) {
512
+ const start = Date.now();
513
+ console.error(`[Executor] connectAll: starting connection to ${endpoints.length} browsers with concurrency limit`);
514
+ const connectionLimiter = new ConcurrencyLimiter(
515
+ this.heavyLimiter.limit
516
+ );
517
+ const tasks = endpoints.map(async (endpoint) => {
518
+ return connectionLimiter.run(async () => {
519
+ const taskStart = Date.now();
520
+ try {
521
+ console.error(`[Executor] Connecting to ${endpoint.id} at ${endpoint.cdpEndpoint}...`);
522
+ const result = await this.connect(endpoint);
523
+ console.error(`[Executor] \u2705 Connected to ${endpoint.id}: ${result.url}`);
524
+ return {
525
+ browserId: endpoint.id,
526
+ success: true,
527
+ result,
528
+ duration: Date.now() - taskStart
529
+ };
530
+ } catch (error) {
531
+ console.error(`[Executor] \u274C Failed to connect to ${endpoint.id}: ${error}`);
532
+ return {
533
+ browserId: endpoint.id,
534
+ success: false,
535
+ error: String(error),
536
+ duration: Date.now() - taskStart
537
+ };
538
+ }
539
+ });
540
+ });
541
+ const results = await Promise.all(tasks);
542
+ console.error(`[Executor] connectAll complete: ${results.filter((r) => r.success).length}/${results.length} connected in ${Date.now() - start}ms`);
543
+ return {
544
+ results,
545
+ totalDuration: Date.now() - start,
546
+ successCount: results.filter((r) => r.success).length,
547
+ failureCount: results.filter((r) => !r.success).length
548
+ };
549
+ }
550
+ async connect(endpoint, retries = 3) {
551
+ if (this.browsers.has(endpoint.id)) {
552
+ throw new Error(`Browser ${endpoint.id} is already connected`);
553
+ }
554
+ let lastError = null;
555
+ for (let attempt = 1; attempt <= retries; attempt++) {
556
+ try {
557
+ console.error(`[Executor] Attempt ${attempt}/${retries} to connect to ${endpoint.id} at ${endpoint.cdpEndpoint}`);
558
+ const browser = await playwright.chromium.connectOverCDP(endpoint.cdpEndpoint, {
559
+ headers: endpoint.cdpHeaders
560
+ });
561
+ return await this._finishConnect(endpoint, browser);
562
+ } catch (error) {
563
+ lastError = error instanceof Error ? error : new Error(String(error));
564
+ console.error(`[Executor] Attempt ${attempt}/${retries} failed for ${endpoint.id}: ${lastError.message}`);
565
+ if (attempt < retries) {
566
+ const delay = Math.pow(2, attempt - 1) * 300;
567
+ console.error(`[Executor] Retrying in ${delay}ms...`);
568
+ await new Promise((r) => setTimeout(r, delay));
569
+ }
570
+ }
571
+ }
572
+ throw lastError || new Error(`Failed to connect to ${endpoint.id} after ${retries} attempts`);
573
+ }
574
+ async _finishConnect(endpoint, browser) {
575
+ let context;
576
+ let page;
577
+ const contexts = browser.contexts();
578
+ console.error(`[Executor] Browser ${endpoint.id} has ${contexts.length} contexts`);
579
+ if (contexts.length > 0) {
580
+ context = contexts[0];
581
+ const pages = context.pages();
582
+ console.error(`[Executor] Context has ${pages.length} pages`);
583
+ if (pages.length > 0) {
584
+ page = pages.find((p) => !p.url().startsWith("about:")) || pages[0];
585
+ console.error(`[Executor] Using existing page: ${page.url()}`);
586
+ } else {
587
+ console.error(`[Executor] No pages in context, waiting for existing page...`);
588
+ const waitStart = Date.now();
589
+ while (Date.now() - waitStart < 2e3) {
590
+ const currentPages = context.pages();
591
+ if (currentPages.length > 0) {
592
+ page = currentPages.find((p) => !p.url().startsWith("about:")) || currentPages[0];
593
+ console.error(`[Executor] Found page after waiting: ${page.url()}`);
594
+ break;
595
+ }
596
+ await new Promise((r) => setTimeout(r, 100));
597
+ }
598
+ if (!page) {
599
+ console.error(`[Executor] No pages appeared after 2s, creating new page (fallback)`);
600
+ page = await context.newPage();
601
+ }
602
+ }
603
+ } else {
604
+ console.error(`[Executor] No contexts, waiting for context to appear...`);
605
+ const waitStart = Date.now();
606
+ while (Date.now() - waitStart < 2e3) {
607
+ const currentContexts = browser.contexts();
608
+ if (currentContexts.length > 0) {
609
+ context = currentContexts[0];
610
+ const pages = context.pages();
611
+ if (pages.length > 0) {
612
+ page = pages.find((p) => !p.url().startsWith("about:")) || pages[0];
613
+ console.error(`[Executor] Found context and page after waiting: ${page.url()}`);
614
+ break;
615
+ }
616
+ }
617
+ await new Promise((r) => setTimeout(r, 100));
618
+ }
619
+ if (!context) {
620
+ console.error(`[Executor] Creating new context and page for ${endpoint.id} (fallback)`);
621
+ context = await browser.newContext();
622
+ page = await context.newPage();
623
+ } else if (!page) {
624
+ console.error(`[Executor] Context found but no pages, creating page (fallback)`);
625
+ page = await context.newPage();
626
+ }
627
+ }
628
+ try {
629
+ const allPages = context.pages();
630
+ for (const p of allPages) {
631
+ if (p !== page && p.url() === "about:blank") {
632
+ await p.close().catch(() => {
633
+ });
634
+ console.error(`[Executor] Closed extra about:blank page for ${endpoint.id}`);
635
+ }
636
+ }
637
+ } catch {
638
+ }
639
+ page.setDefaultTimeout(this.defaultTimeout);
640
+ page.setDefaultNavigationTimeout(6e4);
641
+ const connected = {
642
+ id: endpoint.id,
643
+ browser,
644
+ context,
645
+ page,
646
+ endpoint,
647
+ connectedAt: /* @__PURE__ */ new Date()
648
+ };
649
+ this.browsers.set(endpoint.id, connected);
650
+ console.error(`[Executor] Successfully stored ${endpoint.id} in browsers map. Total browsers: ${this.browsers.size}`);
651
+ this.snapshotRefs.set(endpoint.id, /* @__PURE__ */ new Map());
652
+ const consoleLog = [];
653
+ this.consoleMessages.set(endpoint.id, consoleLog);
654
+ page.on("console", (msg) => {
655
+ consoleLog.push({ type: msg.type(), text: msg.text(), timestamp: Date.now() });
656
+ if (consoleLog.length > 1e3) consoleLog.shift();
657
+ });
658
+ const networkLog = [];
659
+ this.networkRequests.set(endpoint.id, networkLog);
660
+ page.on("response", (response) => {
661
+ const req = response.request();
662
+ networkLog.push({
663
+ url: req.url(),
664
+ method: req.method(),
665
+ status: response.status(),
666
+ resourceType: req.resourceType(),
667
+ timestamp: Date.now()
668
+ });
669
+ if (networkLog.length > 1e3) networkLog.shift();
670
+ });
671
+ browser.on("disconnected", () => {
672
+ console.error(`[Executor] Browser ${endpoint.id} disconnected`);
673
+ this.browsers.delete(endpoint.id);
674
+ this.consoleMessages.delete(endpoint.id);
675
+ this.networkRequests.delete(endpoint.id);
676
+ this.snapshotRefs.delete(endpoint.id);
677
+ setTimeout(async () => {
678
+ if (!this.browsers.has(endpoint.id)) {
679
+ console.error(`[Executor] Attempting auto-reconnect for ${endpoint.id}...`);
680
+ try {
681
+ await this.connect(endpoint, 2);
682
+ console.error(`[Executor] \u2705 Auto-reconnected ${endpoint.id}`);
683
+ } catch (e) {
684
+ console.error(`[Executor] \u274C Auto-reconnect failed for ${endpoint.id}: ${e instanceof Error ? e.message : String(e)}`);
685
+ }
686
+ }
687
+ }, 500);
688
+ });
689
+ const url = page.url();
690
+ const title = await page.title().catch(() => "");
691
+ console.error(`[Executor] Browser ${endpoint.id} connected. URL: ${url}, Title: ${title}`);
692
+ return {
693
+ id: endpoint.id,
694
+ connected: true,
695
+ url,
696
+ title,
697
+ endpoint: endpoint.cdpEndpoint,
698
+ tags: endpoint.tags,
699
+ connectedAt: connected.connectedAt
700
+ };
701
+ }
702
+ async disconnect(browserId) {
703
+ const browser = this.browsers.get(browserId);
704
+ if (!browser) throw new Error(`Browser ${browserId} not found`);
705
+ await browser.browser.close().catch(() => {
706
+ });
707
+ this.browsers.delete(browserId);
708
+ this.consoleMessages.delete(browserId);
709
+ this.networkRequests.delete(browserId);
710
+ this.snapshotRefs.delete(browserId);
711
+ }
712
+ async disconnectAll() {
713
+ const ids = Array.from(this.browsers.keys());
714
+ await Promise.all(
715
+ ids.map((id) => this.disconnect(id).catch(() => {
716
+ }))
717
+ );
718
+ this.browsers.clear();
719
+ }
720
+ async listBrowsers() {
721
+ const statuses = [];
722
+ for (const [id, browser] of this.browsers) {
723
+ try {
724
+ statuses.push({
725
+ id,
726
+ connected: browser.browser.isConnected(),
727
+ url: browser.page.url(),
728
+ title: await browser.page.title().catch(() => ""),
729
+ endpoint: browser.endpoint.cdpEndpoint,
730
+ tags: browser.endpoint.tags,
731
+ connectedAt: browser.connectedAt
732
+ });
733
+ } catch {
734
+ statuses.push({
735
+ id,
736
+ connected: false,
737
+ endpoint: browser.endpoint.cdpEndpoint,
738
+ tags: browser.endpoint.tags
739
+ });
740
+ }
741
+ }
742
+ return statuses;
743
+ }
744
+ /**
745
+ * Detailed status check for all browsers
746
+ */
747
+ async getDetailedStatus(options = {}) {
748
+ const results = [];
749
+ const urlCounts = /* @__PURE__ */ new Map();
750
+ for (const [id, browser] of this.browsers) {
751
+ try {
752
+ if (!browser.browser.isConnected()) {
753
+ results.push({
754
+ id,
755
+ status: "disconnected",
756
+ url: "",
757
+ title: "",
758
+ error: "Browser disconnected",
759
+ responsive: false
760
+ });
761
+ continue;
762
+ }
763
+ const url = browser.page.url();
764
+ const title = await browser.page.title().catch(() => "");
765
+ let responsive = true;
766
+ try {
767
+ let timeoutId;
768
+ const timeoutPromise = new Promise((_, reject) => {
769
+ timeoutId = setTimeout(() => reject(new Error("timeout")), 1e3);
770
+ });
771
+ try {
772
+ await Promise.race([
773
+ browser.page.evaluate(() => true),
774
+ timeoutPromise
775
+ ]);
776
+ clearTimeout(timeoutId);
777
+ } catch {
778
+ clearTimeout(timeoutId);
779
+ responsive = false;
780
+ }
781
+ } catch {
782
+ responsive = false;
783
+ }
784
+ const baseUrl = url.split("?")[0];
785
+ urlCounts.set(baseUrl, (urlCounts.get(baseUrl) || 0) + 1);
786
+ results.push({
787
+ id,
788
+ status: responsive ? "ok" : "error",
789
+ url,
790
+ title,
791
+ error: responsive ? void 0 : "Page not responding",
792
+ responsive
793
+ });
794
+ } catch (error) {
795
+ results.push({
796
+ id,
797
+ status: "error",
798
+ url: "",
799
+ title: "",
800
+ error: String(error),
801
+ responsive: false
802
+ });
803
+ }
804
+ }
805
+ let majorityUrl;
806
+ let maxCount = 0;
807
+ for (const [url, count] of urlCounts) {
808
+ if (count > maxCount) {
809
+ maxCount = count;
810
+ majorityUrl = url;
811
+ }
812
+ }
813
+ if (majorityUrl && maxCount > 1) {
814
+ for (const result of results) {
815
+ if (result.status === "ok" && result.url.split("?")[0] !== majorityUrl) {
816
+ result.status = "behind";
817
+ }
818
+ }
819
+ }
820
+ const okCount = results.filter((r) => r.status === "ok").length;
821
+ return {
822
+ browsers: results,
823
+ summary: {
824
+ total: results.length,
825
+ ok: okCount,
826
+ problems: results.length - okCount
827
+ },
828
+ majorityUrl
829
+ };
830
+ }
831
+ /**
832
+ * Sync browsers from API (on-demand) with retry logic.
833
+ * Called automatically when getBrowserIds finds no connected browsers.
834
+ */
835
+ async syncFromAPI(retries = 2) {
836
+ const apiServerUrl = process.env.API_SERVER_URL;
837
+ const userId = process.env.USER_ID;
838
+ if (!apiServerUrl || !userId) {
839
+ console.error(`[Executor] syncFromAPI: missing API_SERVER_URL or USER_ID`);
840
+ return;
841
+ }
842
+ let lastError = null;
843
+ for (let attempt = 1; attempt <= retries; attempt++) {
844
+ try {
845
+ const url = `${apiServerUrl}/api/browsers?userId=${userId}`;
846
+ console.error(`[Executor] syncFromAPI: fetching ${url} (attempt ${attempt}/${retries})`);
847
+ const controller = new AbortController();
848
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
849
+ try {
850
+ const response = await fetch(url, { signal: controller.signal });
851
+ clearTimeout(timeoutId);
852
+ if (!response.ok) {
853
+ throw new Error(`API returned ${response.status}`);
854
+ }
855
+ const data = await response.json();
856
+ const apiBrowsers = data.browsers || [];
857
+ console.error(`[Executor] syncFromAPI: API returned ${apiBrowsers.length} browsers`);
858
+ const connectedIds = new Set(this.browsers.keys());
859
+ for (const browser of apiBrowsers) {
860
+ if (!connectedIds.has(browser.id)) {
861
+ console.error(`[Executor] syncFromAPI: connecting ${browser.id}`);
862
+ try {
863
+ await this.connect({
864
+ id: browser.id,
865
+ cdpEndpoint: browser.cdpEndpoint,
866
+ tags: browser.tags || []
867
+ });
868
+ console.error(`[Executor] syncFromAPI: \u2713 connected ${browser.id}`);
869
+ } catch (e) {
870
+ console.error(`[Executor] syncFromAPI: \u2717 failed ${browser.id}: ${e instanceof Error ? e.message : String(e)}`);
871
+ }
872
+ }
873
+ }
874
+ const apiIds = new Set(apiBrowsers.map((b) => b.id));
875
+ for (const id of connectedIds) {
876
+ if (!apiIds.has(id)) {
877
+ console.error(`[Executor] syncFromAPI: disconnecting stale ${id}`);
878
+ try {
879
+ await this.disconnect(id);
880
+ } catch {
881
+ }
882
+ }
883
+ }
884
+ console.error(`[Executor] syncFromAPI: done, now have ${this.browsers.size} browsers`);
885
+ this.lastSyncTime = Date.now();
886
+ return;
887
+ } catch (e) {
888
+ clearTimeout(timeoutId);
889
+ throw e;
890
+ }
891
+ } catch (e) {
892
+ lastError = e instanceof Error ? e : new Error(String(e));
893
+ console.error(`[Executor] syncFromAPI: attempt ${attempt}/${retries} failed: ${lastError.message}`);
894
+ if (attempt < retries) {
895
+ const delay = Math.pow(2, attempt - 1) * 300;
896
+ console.error(`[Executor] syncFromAPI: retrying in ${delay}ms...`);
897
+ await new Promise((r) => setTimeout(r, delay));
898
+ }
899
+ }
900
+ }
901
+ console.error(`[Executor] syncFromAPI: all ${retries} attempts failed`);
902
+ }
903
+ getBrowserIds(filter) {
904
+ let ids = Array.from(this.browsers.keys());
905
+ if (filter?.browserIds?.length) {
906
+ ids = ids.filter((id) => filter.browserIds.includes(id));
907
+ }
908
+ if (filter?.tags?.length) {
909
+ ids = ids.filter((id) => {
910
+ const browser = this.browsers.get(id);
911
+ return browser?.endpoint.tags?.some((t) => filter.tags.includes(t));
912
+ });
913
+ }
914
+ return ids;
915
+ }
916
+ /**
917
+ * Ensure browsers are loaded before operations.
918
+ * Uses caching to prevent excessive API calls.
919
+ * Only syncs if: no browsers connected AND cache expired.
920
+ */
921
+ async ensureBrowsersLoaded() {
922
+ if (this.browsers.size > 0) {
923
+ return;
924
+ }
925
+ const now = Date.now();
926
+ if (now - this.lastSyncTime < _MultiBrowserExecutor.SYNC_TTL_MS) {
927
+ console.error(`[Executor] ensureBrowsersLoaded: skipping sync, cache valid (${now - this.lastSyncTime}ms old)`);
928
+ return;
929
+ }
930
+ if (this.syncInProgress) {
931
+ console.error(`[Executor] ensureBrowsersLoaded: sync already in progress, waiting...`);
932
+ await this.syncInProgress;
933
+ return;
934
+ }
935
+ console.error(`[Executor] ensureBrowsersLoaded: no browsers, syncing from API...`);
936
+ this.syncInProgress = this.syncFromAPI().finally(() => {
937
+ this.syncInProgress = null;
938
+ });
939
+ await this.syncInProgress;
940
+ }
941
+ getBrowserEndpoint(browserId) {
942
+ const browser = this.browsers.get(browserId);
943
+ return browser?.endpoint;
944
+ }
945
+ getPage(browserId) {
946
+ const browser = this.browsers.get(browserId);
947
+ if (!browser) throw new Error(`Browser ${browserId} not found`);
948
+ return browser.page;
949
+ }
950
+ resolveSelector(target, browserId) {
951
+ if (typeof target === "string") return target;
952
+ if (target.selector) return target.selector;
953
+ if (target.ref) {
954
+ const selector = this.snapshotRefs.get(browserId)?.get(String(target.ref));
955
+ if (selector) return selector;
956
+ throw new Error(`Unknown ref "${target.ref}" for browser ${browserId}. Call browser_parallel_snapshot first.`);
957
+ }
958
+ throw new Error("Either selector or ref is required");
959
+ }
960
+ isTruthyFieldValue(value) {
961
+ return ["true", "1", "yes", "on", "checked"].includes(value.trim().toLowerCase());
962
+ }
963
+ async waitForDomStable(page, stableMs, timeoutMs) {
964
+ const startedAt = Date.now();
965
+ let lastSignature = "";
966
+ let stableSince = 0;
967
+ const pollInterval = Math.min(250, Math.max(75, Math.floor(stableMs / 3) || 100));
968
+ while (Date.now() - startedAt <= timeoutMs) {
969
+ const signature = await page.evaluate(() => JSON.stringify({
970
+ readyState: document.readyState,
971
+ nodeCount: document.querySelectorAll("*").length,
972
+ textLength: document.body?.innerText.length ?? 0,
973
+ scrollHeight: document.documentElement?.scrollHeight ?? 0,
974
+ scrollWidth: document.documentElement?.scrollWidth ?? 0
975
+ }));
976
+ if (signature === lastSignature) {
977
+ if (!stableSince) stableSince = Date.now();
978
+ if (Date.now() - stableSince >= stableMs) return;
979
+ } else {
980
+ lastSignature = signature;
981
+ stableSince = 0;
982
+ }
983
+ await page.waitForTimeout(pollInterval);
984
+ }
985
+ throw new Error(`DOM did not become stable within ${timeoutMs}ms`);
986
+ }
987
+ getMimeType(fileName) {
988
+ const ext = extname(fileName).toLowerCase();
989
+ const types = {
990
+ ".txt": "text/plain",
991
+ ".json": "application/json",
992
+ ".csv": "text/csv",
993
+ ".pdf": "application/pdf",
994
+ ".png": "image/png",
995
+ ".jpg": "image/jpeg",
996
+ ".jpeg": "image/jpeg",
997
+ ".gif": "image/gif",
998
+ ".webp": "image/webp",
999
+ ".svg": "image/svg+xml",
1000
+ ".zip": "application/zip"
1001
+ };
1002
+ return types[ext] || "application/octet-stream";
1003
+ }
1004
+ // ==================== PARALLEL EXECUTION ====================
1005
+ /**
1006
+ * Get the appropriate limiter based on operation type
1007
+ */
1008
+ getLimiter(type) {
1009
+ if (typeof type === "number") {
1010
+ return new ConcurrencyLimiter(type);
1011
+ }
1012
+ switch (type) {
1013
+ case "heavy":
1014
+ return this.heavyLimiter;
1015
+ case "light":
1016
+ return this.lightLimiter;
1017
+ default:
1018
+ return this.limiter;
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Execute action on multiple browsers with controlled concurrency.
1023
+ * This prevents system overload when operating on 9+ browsers.
1024
+ * Concurrency is auto-detected based on system resources (RAM, CPU cores).
1025
+ *
1026
+ * @param browserIds - specific browsers to target, or undefined for all
1027
+ * @param action - async function to execute on each browser
1028
+ * @param options.timeout - per-browser timeout
1029
+ * @param options.concurrency - 'heavy' | 'light' | number - type of operation or explicit limit
1030
+ */
1031
+ async executeParallel(browserIds, action, options = {}) {
1032
+ const start = Date.now();
1033
+ await this.ensureBrowsersLoaded();
1034
+ const ids = browserIds ?? this.getBrowserIds();
1035
+ const timeout = options.timeout ?? this.defaultTimeout;
1036
+ const limiter = this.getLimiter(options.concurrency);
1037
+ console.error(`[Executor] executeParallel: running on ${ids.length} browsers with concurrency limit: [${ids.join(", ")}]`);
1038
+ const tasks = ids.map(async (id) => {
1039
+ return limiter.run(async () => {
1040
+ const taskStart = Date.now();
1041
+ const browser = this.browsers.get(id);
1042
+ if (!browser) {
1043
+ console.error(`[Executor] \u274C Browser ${id} not found in map`);
1044
+ return {
1045
+ browserId: id,
1046
+ success: false,
1047
+ error: `Browser ${id} not found`,
1048
+ duration: Date.now() - taskStart
1049
+ };
1050
+ }
1051
+ if (!browser.browser.isConnected()) {
1052
+ console.error(`[Executor] \u274C Browser ${id} is disconnected`);
1053
+ this.browsers.delete(id);
1054
+ return {
1055
+ browserId: id,
1056
+ success: false,
1057
+ error: `Browser ${id} is disconnected`,
1058
+ duration: Date.now() - taskStart
1059
+ };
1060
+ }
1061
+ try {
1062
+ console.error(`[Executor] \u{1F504} Executing action on ${id}...`);
1063
+ let timeoutId;
1064
+ const timeoutPromise = new Promise((_, reject) => {
1065
+ timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout);
1066
+ });
1067
+ try {
1068
+ const result = await Promise.race([
1069
+ action(browser.page, id),
1070
+ timeoutPromise
1071
+ ]);
1072
+ clearTimeout(timeoutId);
1073
+ console.error(`[Executor] \u2705 Action completed on ${id}`);
1074
+ return {
1075
+ browserId: id,
1076
+ success: true,
1077
+ result,
1078
+ duration: Date.now() - taskStart
1079
+ };
1080
+ } catch (error) {
1081
+ clearTimeout(timeoutId);
1082
+ throw error;
1083
+ }
1084
+ } catch (error) {
1085
+ console.error(`[Executor] \u274C Action failed on ${id}: ${error}`);
1086
+ return {
1087
+ browserId: id,
1088
+ success: false,
1089
+ error: String(error),
1090
+ duration: Date.now() - taskStart
1091
+ };
1092
+ }
1093
+ });
1094
+ });
1095
+ const results = await Promise.all(tasks);
1096
+ console.error(`[Executor] executeParallel complete: ${results.filter((r) => r.success).length}/${results.length} succeeded in ${Date.now() - start}ms`);
1097
+ return {
1098
+ results,
1099
+ totalDuration: Date.now() - start,
1100
+ successCount: results.filter((r) => r.success).length,
1101
+ failureCount: results.filter((r) => !r.success).length
1102
+ };
1103
+ }
1104
+ // ==================== NAVIGATION ====================
1105
+ /**
1106
+ * Navigate multiple browsers to the same URL.
1107
+ * OPTIMIZATION: Uses higher concurrency for navigation (it's mostly network-bound)
1108
+ */
1109
+ async parallelNavigate(url, browserIds) {
1110
+ return this.executeParallel(
1111
+ browserIds,
1112
+ async (page) => {
1113
+ await page.goto(url, { waitUntil: "domcontentloaded" });
1114
+ await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
1115
+ });
1116
+ return { url: page.url(), title: await page.title() };
1117
+ },
1118
+ {
1119
+ timeout: 6e4,
1120
+ concurrency: "light"
1121
+ }
1122
+ );
1123
+ }
1124
+ async parallelNavigateMulti(targets) {
1125
+ const start = Date.now();
1126
+ const tasks = targets.map(async (target) => {
1127
+ const taskStart = Date.now();
1128
+ const browser = this.browsers.get(target.browserId);
1129
+ if (!browser) {
1130
+ return {
1131
+ browserId: target.browserId,
1132
+ success: false,
1133
+ error: `Browser ${target.browserId} not found`,
1134
+ duration: Date.now() - taskStart
1135
+ };
1136
+ }
1137
+ try {
1138
+ await browser.page.goto(target.url, { waitUntil: "domcontentloaded" });
1139
+ await browser.page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
1140
+ });
1141
+ return {
1142
+ browserId: target.browserId,
1143
+ success: true,
1144
+ result: { url: browser.page.url(), title: await browser.page.title() },
1145
+ duration: Date.now() - taskStart
1146
+ };
1147
+ } catch (error) {
1148
+ return {
1149
+ browserId: target.browserId,
1150
+ success: false,
1151
+ error: String(error),
1152
+ duration: Date.now() - taskStart
1153
+ };
1154
+ }
1155
+ });
1156
+ const results = await Promise.all(tasks);
1157
+ return {
1158
+ results,
1159
+ totalDuration: Date.now() - start,
1160
+ successCount: results.filter((r) => r.success).length,
1161
+ failureCount: results.filter((r) => !r.success).length
1162
+ };
1163
+ }
1164
+ async parallelGoBack(browserIds) {
1165
+ return this.executeParallel(browserIds, async (page) => {
1166
+ await page.goBack();
1167
+ });
1168
+ }
1169
+ async parallelGoForward(browserIds) {
1170
+ return this.executeParallel(browserIds, async (page) => {
1171
+ await page.goForward();
1172
+ });
1173
+ }
1174
+ async parallelReload(browserIds) {
1175
+ return this.executeParallel(browserIds, async (page) => {
1176
+ await page.reload();
1177
+ });
1178
+ }
1179
+ // ==================== SNAPSHOT ====================
1180
+ async parallelSnapshot(browserIds, options = {}) {
1181
+ const MAX_SNAPSHOT_SIZE = 3e4;
1182
+ return this.executeParallel(
1183
+ browserIds,
1184
+ async (page, browserId) => {
1185
+ const elements = await page.evaluate((compactMode) => {
1186
+ const interactiveTags = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea", "summary"]);
1187
+ const interactiveRoles = /* @__PURE__ */ new Set([
1188
+ "button",
1189
+ "link",
1190
+ "textbox",
1191
+ "combobox",
1192
+ "searchbox",
1193
+ "checkbox",
1194
+ "radio",
1195
+ "switch",
1196
+ "slider",
1197
+ "spinbutton",
1198
+ "menuitem",
1199
+ "menuitemcheckbox",
1200
+ "menuitemradio",
1201
+ "option",
1202
+ "tab",
1203
+ "treeitem",
1204
+ "listbox"
1205
+ ]);
1206
+ function buildSelector(el) {
1207
+ const tag = el.tagName.toLowerCase();
1208
+ if (el.id) return `#${CSS.escape(el.id)}`;
1209
+ const testId = el.getAttribute("data-testid");
1210
+ if (testId) return `[data-testid="${testId}"]`;
1211
+ const name = el.getAttribute("name");
1212
+ if (name) return `${tag}[name="${name}"]`;
1213
+ const placeholder = el.getAttribute("placeholder");
1214
+ if (placeholder) return `${tag}[placeholder="${placeholder}"]`;
1215
+ const ariaLabel = el.getAttribute("aria-label");
1216
+ if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
1217
+ const role = el.getAttribute("role");
1218
+ if (role) {
1219
+ const text = el.textContent?.trim().slice(0, 40);
1220
+ if (text) return `role=${role}[name="${text}"]`;
1221
+ return `[role="${role}"]`;
1222
+ }
1223
+ if (tag === "button" || tag === "a") {
1224
+ const text = el.textContent?.trim().slice(0, 40);
1225
+ if (text) return `${tag}:has-text("${text}")`;
1226
+ }
1227
+ const type = el.getAttribute("type");
1228
+ if (tag === "input" && type) return `input[type="${type}"]`;
1229
+ if (el.className && typeof el.className === "string") {
1230
+ const cls = el.className.trim().split(/\s+/)[0];
1231
+ if (cls) return `${tag}.${CSS.escape(cls)}`;
1232
+ }
1233
+ return tag;
1234
+ }
1235
+ function findSection(el) {
1236
+ let node = el;
1237
+ while (node) {
1238
+ let sibling = node.previousElementSibling;
1239
+ while (sibling) {
1240
+ if (/^H[1-6]$/.test(sibling.tagName)) {
1241
+ return sibling.textContent?.trim().slice(0, 50) || "";
1242
+ }
1243
+ sibling = sibling.previousElementSibling;
1244
+ }
1245
+ const role = node.getAttribute("role");
1246
+ const ariaLabel = node.getAttribute("aria-label");
1247
+ if (role === "navigation" || role === "main" || role === "banner" || role === "dialog" || role === "form") {
1248
+ return `[${role}${ariaLabel ? ": " + ariaLabel : ""}]`;
1249
+ }
1250
+ if (node.tagName === "NAV" || node.tagName === "MAIN" || node.tagName === "HEADER" || node.tagName === "FOOTER" || node.tagName === "FORM") {
1251
+ const label = ariaLabel || node.getAttribute("name") || "";
1252
+ return `[${node.tagName.toLowerCase()}${label ? ": " + label : ""}]`;
1253
+ }
1254
+ node = node.parentElement;
1255
+ }
1256
+ return "";
1257
+ }
1258
+ const results = [];
1259
+ const seen = /* @__PURE__ */ new Set();
1260
+ if (!compactMode) {
1261
+ const headings = Array.from(document.querySelectorAll("h1, h2, h3"));
1262
+ for (const h of headings) {
1263
+ const text = h.textContent?.trim().slice(0, 80);
1264
+ if (text) {
1265
+ results.push({
1266
+ tag: h.tagName.toLowerCase(),
1267
+ selector: "",
1268
+ text
1269
+ });
1270
+ }
1271
+ }
1272
+ }
1273
+ const allElements = Array.from(document.querySelectorAll("*"));
1274
+ for (const el of allElements) {
1275
+ const tag = el.tagName.toLowerCase();
1276
+ const role = el.getAttribute("role");
1277
+ const isInteractive = interactiveTags.has(tag) || role !== null && interactiveRoles.has(role) || el.getAttribute("contenteditable") === "true" || el.getAttribute("tabindex") !== null && el.getAttribute("tabindex") !== "-1";
1278
+ if (!isInteractive) continue;
1279
+ const rect = el.getBoundingClientRect();
1280
+ if (rect.width === 0 && rect.height === 0) continue;
1281
+ const style = window.getComputedStyle(el);
1282
+ if (style.display === "none" || style.visibility === "hidden") continue;
1283
+ const selector = buildSelector(el);
1284
+ if (seen.has(selector)) continue;
1285
+ seen.add(selector);
1286
+ const text = el.textContent?.trim().slice(0, 60) || "";
1287
+ const info = { tag, selector, text };
1288
+ const type = el.getAttribute("type");
1289
+ if (type) info.type = type;
1290
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1291
+ if (el.value) info.value = el.value.slice(0, 60);
1292
+ }
1293
+ if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) {
1294
+ info.checked = el.checked;
1295
+ }
1296
+ if (el.getAttribute("disabled") !== null) {
1297
+ info.disabled = true;
1298
+ }
1299
+ const href = el.getAttribute("href");
1300
+ if (href && href !== "#" && !href.startsWith("javascript:")) {
1301
+ info.href = href.slice(0, 100);
1302
+ }
1303
+ const placeholder = el.getAttribute("placeholder");
1304
+ if (placeholder) info.placeholder = placeholder;
1305
+ if (!compactMode) {
1306
+ const section = findSection(el);
1307
+ if (section) info.section = section;
1308
+ }
1309
+ results.push(info);
1310
+ }
1311
+ return results;
1312
+ }, !!options.compact);
1313
+ const refMap = /* @__PURE__ */ new Map();
1314
+ let nextRef = 1;
1315
+ const elementsWithRefs = elements.map((element) => {
1316
+ if (!element.selector) return element;
1317
+ const ref = String(nextRef++);
1318
+ refMap.set(ref, element.selector);
1319
+ return { ...element, ref };
1320
+ });
1321
+ this.snapshotRefs.set(browserId, refMap);
1322
+ let snapshot = this.formatSnapshot(elementsWithRefs, !!options.compact);
1323
+ if (snapshot.length > MAX_SNAPSHOT_SIZE) {
1324
+ snapshot = snapshot.substring(0, MAX_SNAPSHOT_SIZE) + `
1325
+
1326
+ ... [TRUNCATED: ${elementsWithRefs.length} elements total, showing first ${MAX_SNAPSHOT_SIZE} chars]`;
1327
+ }
1328
+ return {
1329
+ browserId,
1330
+ snapshot,
1331
+ url: page.url(),
1332
+ title: await page.title()
1333
+ };
1334
+ },
1335
+ { concurrency: "heavy" }
1336
+ );
1337
+ }
1338
+ /**
1339
+ * Format extracted elements into readable snapshot text
1340
+ */
1341
+ formatSnapshot(elements, compact) {
1342
+ const lines = [];
1343
+ let lastSection = "";
1344
+ for (const el of elements) {
1345
+ if (!el.selector && (el.tag === "h1" || el.tag === "h2" || el.tag === "h3")) {
1346
+ lines.push(`
1347
+ ## ${el.text}`);
1348
+ continue;
1349
+ }
1350
+ if (!compact && el.section && el.section !== lastSection) {
1351
+ lastSection = el.section;
1352
+ lines.push(`
1353
+ --- ${el.section} ---`);
1354
+ }
1355
+ let line = `- ${el.tag}`;
1356
+ if (el.type) line += `[type=${el.type}]`;
1357
+ if (el.disabled) line += " (disabled)";
1358
+ if (el.ref) line += ` [ref=${el.ref}]`;
1359
+ line += ` \u2192 ${el.selector}`;
1360
+ const details = [];
1361
+ if (el.placeholder) details.push(`placeholder="${el.placeholder}"`);
1362
+ if (el.value) details.push(`value="${el.value}"`);
1363
+ if (el.checked !== void 0) details.push(el.checked ? "checked" : "unchecked");
1364
+ if (el.text && el.text.length > 0) details.push(`"${el.text}"`);
1365
+ if (el.href) details.push(`\u2192 ${el.href}`);
1366
+ if (details.length > 0) {
1367
+ line += ` ${details.join(" | ")}`;
1368
+ }
1369
+ lines.push(line);
1370
+ }
1371
+ return lines.join("\n");
1372
+ }
1373
+ /**
1374
+ * Take screenshots from multiple browsers.
1375
+ * OPTIMIZATION: Uses JPEG format with quality 70% for ~5x smaller files.
1376
+ * For 9 browsers: ~4.5MB PNG → ~900KB JPEG
1377
+ */
1378
+ async parallelScreenshot(browserIds, options = {}) {
1379
+ const format = options.format ?? "jpeg";
1380
+ const quality = format === "jpeg" ? options.quality ?? 70 : void 0;
1381
+ return this.executeParallel(
1382
+ browserIds,
1383
+ async (page) => {
1384
+ return await page.screenshot({
1385
+ fullPage: options.fullPage,
1386
+ type: format,
1387
+ quality
1388
+ });
1389
+ },
1390
+ { concurrency: "heavy" }
1391
+ );
1392
+ }
1393
+ // ==================== HUMAN-LIKE HELPERS ====================
1394
+ /** Get or create a human profile for a browser */
1395
+ getProfile(browserId) {
1396
+ let profile = this.profiles.get(browserId);
1397
+ if (!profile) {
1398
+ profile = generateProfile(browserId);
1399
+ this.profiles.set(browserId, profile);
1400
+ }
1401
+ return profile;
1402
+ }
1403
+ /** Get last known mouse position, or a random starting position */
1404
+ getMousePos(browserId) {
1405
+ return this.lastMousePosition.get(browserId) ?? {
1406
+ x: 200 + Math.random() * 400,
1407
+ y: 150 + Math.random() * 300
1408
+ };
1409
+ }
1410
+ /** Update tracked mouse position */
1411
+ setMousePos(browserId, pos) {
1412
+ this.lastMousePosition.set(browserId, pos);
1413
+ }
1414
+ // ==================== REF-BASED INTERACTION (human-like) ====================
1415
+ async parallelClick(target, browserIds) {
1416
+ return this.executeParallel(browserIds, async (page, browserId) => {
1417
+ const profile = this.getProfile(browserId);
1418
+ const selector = this.resolveSelector(target, browserId);
1419
+ const locator = page.locator(selector);
1420
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1421
+ await ensureCursor(page);
1422
+ await preActionDelay(profile);
1423
+ const { target: clickTarget } = await getClickTarget(page, locator);
1424
+ const fromPos = this.getMousePos(browserId);
1425
+ await humanClick(page, clickTarget, fromPos, profile);
1426
+ this.setMousePos(browserId, clickTarget);
1427
+ });
1428
+ }
1429
+ async parallelFill(target, text, browserIds) {
1430
+ return this.executeParallel(browserIds, async (page, browserId) => {
1431
+ const profile = this.getProfile(browserId);
1432
+ const selector = this.resolveSelector(target, browserId);
1433
+ const locator = page.locator(selector);
1434
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1435
+ await ensureCursor(page);
1436
+ await preActionDelay(profile);
1437
+ const { target: clickTarget } = await getClickTarget(page, locator);
1438
+ const fromPos = this.getMousePos(browserId);
1439
+ await humanClick(page, clickTarget, fromPos, profile);
1440
+ this.setMousePos(browserId, clickTarget);
1441
+ await humanDelay(80, 200);
1442
+ await page.keyboard.press("Meta+a");
1443
+ await humanDelay(30, 80);
1444
+ await page.keyboard.press("Backspace");
1445
+ await humanDelay(50, 150);
1446
+ await humanTypeText(page, text, profile);
1447
+ return `filled "${text}"`;
1448
+ });
1449
+ }
1450
+ async parallelFillMulti(target, texts) {
1451
+ const browserIds = Object.keys(texts);
1452
+ return this.executeParallel(browserIds, async (page, browserId) => {
1453
+ const text = texts[browserId];
1454
+ if (!text) throw new Error(`No text provided for browser ${browserId}`);
1455
+ const profile = this.getProfile(browserId);
1456
+ const selector = this.resolveSelector(target, browserId);
1457
+ const locator = page.locator(selector);
1458
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1459
+ await ensureCursor(page);
1460
+ await preActionDelay(profile);
1461
+ const { target: clickTarget } = await getClickTarget(page, locator);
1462
+ const fromPos = this.getMousePos(browserId);
1463
+ await humanClick(page, clickTarget, fromPos, profile);
1464
+ this.setMousePos(browserId, clickTarget);
1465
+ await humanDelay(80, 200);
1466
+ await page.keyboard.press("Meta+a");
1467
+ await humanDelay(30, 80);
1468
+ await page.keyboard.press("Backspace");
1469
+ await humanDelay(50, 150);
1470
+ await humanTypeText(page, text, profile);
1471
+ });
1472
+ }
1473
+ async parallelType(target, text, browserIds) {
1474
+ return this.executeParallel(browserIds, async (page, browserId) => {
1475
+ const profile = this.getProfile(browserId);
1476
+ const selector = this.resolveSelector(target, browserId);
1477
+ const locator = page.locator(selector);
1478
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1479
+ await ensureCursor(page);
1480
+ const { target: clickTarget } = await getClickTarget(page, locator);
1481
+ const fromPos = this.getMousePos(browserId);
1482
+ await humanClick(page, clickTarget, fromPos, profile);
1483
+ this.setMousePos(browserId, clickTarget);
1484
+ await humanDelay(50, 150);
1485
+ await humanTypeText(page, text, profile);
1486
+ });
1487
+ }
1488
+ async parallelHover(target, browserIds) {
1489
+ return this.executeParallel(browserIds, async (page, browserId) => {
1490
+ const profile = this.getProfile(browserId);
1491
+ const selector = this.resolveSelector(target, browserId);
1492
+ const locator = page.locator(selector);
1493
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1494
+ await ensureCursor(page);
1495
+ await preActionDelay(profile);
1496
+ const { target: hoverTarget } = await getClickTarget(page, locator);
1497
+ const fromPos = this.getMousePos(browserId);
1498
+ await humanMouseMove(page, fromPos, hoverTarget, profile);
1499
+ this.setMousePos(browserId, hoverTarget);
1500
+ });
1501
+ }
1502
+ async parallelSelectOption(target, values, browserIds) {
1503
+ return this.executeParallel(browserIds, async (page, browserId) => {
1504
+ const profile = this.getProfile(browserId);
1505
+ const selector = this.resolveSelector(target, browserId);
1506
+ const locator = page.locator(selector);
1507
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1508
+ await ensureCursor(page);
1509
+ await preActionDelay(profile);
1510
+ const { target: clickTarget } = await getClickTarget(page, locator);
1511
+ const fromPos = this.getMousePos(browserId);
1512
+ await humanClick(page, clickTarget, fromPos, profile);
1513
+ this.setMousePos(browserId, clickTarget);
1514
+ await humanDelay(200, 500);
1515
+ return await locator.selectOption(values);
1516
+ });
1517
+ }
1518
+ async parallelPressKey(key, browserIds) {
1519
+ return this.executeParallel(browserIds, async (page, browserId) => {
1520
+ await preActionDelay(this.getProfile(browserId));
1521
+ await page.keyboard.press(key);
1522
+ });
1523
+ }
1524
+ async parallelClickCoordinates(coordinates, options = {}) {
1525
+ const browserIds = Object.keys(coordinates);
1526
+ return this.executeParallel(
1527
+ browserIds,
1528
+ async (page, browserId) => {
1529
+ const profile = this.getProfile(browserId);
1530
+ const target = coordinates[browserId];
1531
+ if (!target) {
1532
+ throw new Error(`No coordinates provided for browser ${browserId}`);
1533
+ }
1534
+ const viewport = page.viewportSize();
1535
+ if (!viewport) {
1536
+ throw new Error("Viewport size is not available for the page");
1537
+ }
1538
+ await ensureCursor(page);
1539
+ await preActionDelay(profile);
1540
+ if (options.fromViewport !== false) {
1541
+ const clickX = Math.max(0, Math.min(Math.round(target.x), viewport.width - 1));
1542
+ const clickY = Math.max(0, Math.min(Math.round(target.y), viewport.height - 1));
1543
+ const fromPos = this.getMousePos(browserId);
1544
+ await humanClick(page, { x: clickX, y: clickY }, fromPos, profile);
1545
+ this.setMousePos(browserId, { x: clickX, y: clickY });
1546
+ const scrollY = await page.evaluate(() => globalThis.scrollY ?? 0);
1547
+ return {
1548
+ clickedAt: { x: clickX, y: clickY },
1549
+ documentY: scrollY + clickY,
1550
+ scrollY
1551
+ };
1552
+ } else {
1553
+ const desiredCenter = options.center ?? viewport.height / 2;
1554
+ const desiredScroll = Math.max(0, Math.floor(target.y - desiredCenter));
1555
+ await page.evaluate((scrollY) => {
1556
+ globalThis.scrollTo?.(0, scrollY);
1557
+ }, desiredScroll);
1558
+ const actualScroll = await page.evaluate(() => globalThis.scrollY ?? 0);
1559
+ const clickX = Math.max(0, Math.min(Math.round(target.x), viewport.width - 1));
1560
+ const clickY = Math.round(target.y - actualScroll);
1561
+ if (clickY < 0 || clickY > viewport.height) {
1562
+ throw new Error(
1563
+ `Target Y=${target.y} is outside viewport after scrolling (scrollY=${actualScroll}, viewportHeight=${viewport.height})`
1564
+ );
1565
+ }
1566
+ const fromPos = this.getMousePos(browserId);
1567
+ await humanClick(page, { x: clickX, y: clickY }, fromPos, profile);
1568
+ this.setMousePos(browserId, { x: clickX, y: clickY });
1569
+ return {
1570
+ clickedAt: { x: clickX, y: clickY },
1571
+ documentY: target.y,
1572
+ scrollY: actualScroll
1573
+ };
1574
+ }
1575
+ },
1576
+ { timeout: options.timeout ?? this.defaultTimeout }
1577
+ );
1578
+ }
1579
+ /**
1580
+ * Click at center of normalized bounding box (from OmniParser output)
1581
+ */
1582
+ async parallelClickNormalizedBox(box, browserIds, options = {}) {
1583
+ const [x1, y1, x2, y2] = box;
1584
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1 || y1 < 0 || y1 > 1 || y2 < 0 || y2 > 1) {
1585
+ throw new Error("Coordinates must be normalized (0-1 range)");
1586
+ }
1587
+ const normalizedX = (x1 + x2) / 2;
1588
+ const normalizedY = (y1 + y2) / 2;
1589
+ return this.executeParallel(
1590
+ browserIds,
1591
+ async (page, browserId) => {
1592
+ const profile = this.getProfile(browserId);
1593
+ const viewport = page.viewportSize();
1594
+ if (!viewport) {
1595
+ throw new Error("Viewport size is not available");
1596
+ }
1597
+ const absoluteX = Math.round(normalizedX * viewport.width);
1598
+ const absoluteY = Math.round(normalizedY * viewport.height);
1599
+ await ensureCursor(page);
1600
+ await preActionDelay(profile);
1601
+ const fromPos = this.getMousePos(browserId);
1602
+ await humanMouseMove(page, fromPos, { x: absoluteX, y: absoluteY }, profile);
1603
+ await page.mouse.down({ button: options.button || "left" });
1604
+ await humanDelay(50, 120);
1605
+ await page.mouse.up({ button: options.button || "left" });
1606
+ if (options.clickCount && options.clickCount > 1) {
1607
+ for (let i = 1; i < options.clickCount; i++) {
1608
+ await humanDelay(80, 150);
1609
+ await page.mouse.down({ button: options.button || "left" });
1610
+ await humanDelay(30, 80);
1611
+ await page.mouse.up({ button: options.button || "left" });
1612
+ }
1613
+ }
1614
+ this.setMousePos(browserId, { x: absoluteX, y: absoluteY });
1615
+ return { absoluteX, absoluteY, viewport };
1616
+ },
1617
+ { timeout: options.timeout ?? this.defaultTimeout }
1618
+ );
1619
+ }
1620
+ // ==================== CODE EXECUTION ====================
1621
+ async parallelRunCode(code, browserIds) {
1622
+ return this.executeParallel(browserIds, async (page) => {
1623
+ const AsyncFunction = Object.getPrototypeOf(async function() {
1624
+ }).constructor;
1625
+ const fn = new AsyncFunction("page", code);
1626
+ return await fn(page);
1627
+ });
1628
+ }
1629
+ async parallelRunCodeWithVariables(codeTemplate, variables) {
1630
+ const browserIds = Object.keys(variables);
1631
+ return this.executeParallel(browserIds, async (page, browserId) => {
1632
+ const vars = variables[browserId] || {};
1633
+ let code = codeTemplate;
1634
+ for (const [key, value] of Object.entries(vars)) {
1635
+ code = code.replace(new RegExp(`{{${key}}}`, "g"), value);
1636
+ }
1637
+ const AsyncFunction = Object.getPrototypeOf(async function() {
1638
+ }).constructor;
1639
+ const fn = new AsyncFunction("page", code);
1640
+ return await fn(page);
1641
+ });
1642
+ }
1643
+ async parallelEvaluate(script, browserIds) {
1644
+ return this.executeParallel(browserIds, async (page) => {
1645
+ return await page.evaluate(script);
1646
+ });
1647
+ }
1648
+ // ==================== WAITING ====================
1649
+ async parallelWaitFor(options, browserIds) {
1650
+ const waitTimeMs = options.time ? options.time * 1e3 : 0;
1651
+ const textTimeout = options.timeoutMs ?? 3e4;
1652
+ const domStableTimeout = options.timeoutMs ?? Math.max(options.domStableMs ?? 0, 5e3);
1653
+ const timeout = waitTimeMs + Math.max(textTimeout, domStableTimeout) + 5e3;
1654
+ return this.executeParallel(browserIds, async (page) => {
1655
+ if (options.time) {
1656
+ await page.waitForTimeout(options.time * 1e3);
1657
+ }
1658
+ if (options.text) {
1659
+ await page.getByText(options.text).waitFor({ timeout: textTimeout });
1660
+ }
1661
+ if (options.textGone) {
1662
+ await page.getByText(options.textGone).waitFor({ state: "hidden", timeout: textTimeout });
1663
+ }
1664
+ if (options.domStableMs) {
1665
+ await this.waitForDomStable(page, options.domStableMs, domStableTimeout);
1666
+ }
1667
+ }, { timeout });
1668
+ }
1669
+ // ==================== FILE OPERATIONS (downloads, uploads, drag&drop) ====================
1670
+ /** Cached project files directory path */
1671
+ projectFilesDir = void 0;
1672
+ /** Fetch project files directory from API (cached) */
1673
+ async getProjectFilesDir() {
1674
+ if (this.projectFilesDir !== void 0) return this.projectFilesDir;
1675
+ const apiUrl = process.env.API_SERVER_URL;
1676
+ const userId = process.env.USER_ID;
1677
+ const deviceId = process.env.DEVICE_ID;
1678
+ if (!apiUrl || !userId) {
1679
+ this.projectFilesDir = null;
1680
+ return null;
1681
+ }
1682
+ try {
1683
+ const params = new URLSearchParams({ userId });
1684
+ if (deviceId) params.set("deviceId", deviceId);
1685
+ const resp = await fetch(`${apiUrl}/api/project-path?${params}`);
1686
+ const data = await resp.json();
1687
+ this.projectFilesDir = data.localPath;
1688
+ console.error(`[Executor] Project files dir: ${this.projectFilesDir}`);
1689
+ return this.projectFilesDir;
1690
+ } catch (e) {
1691
+ console.error(`[Executor] Failed to fetch project path:`, e);
1692
+ this.projectFilesDir = null;
1693
+ return null;
1694
+ }
1695
+ }
1696
+ /** Configure browser to download files to project directory */
1697
+ async setupDownloads(browserIds) {
1698
+ const dir = await this.getProjectFilesDir();
1699
+ if (!dir) {
1700
+ throw new Error("Project files directory not available \u2014 file relay may not be connected");
1701
+ }
1702
+ return this.executeParallel(browserIds, async (page) => {
1703
+ const cdp = await page.context().newCDPSession(page);
1704
+ try {
1705
+ await cdp.send("Page.setDownloadBehavior", {
1706
+ behavior: "allow",
1707
+ downloadPath: dir
1708
+ });
1709
+ } finally {
1710
+ await cdp.detach().catch(() => {
1711
+ });
1712
+ }
1713
+ return `Downloads \u2192 ${dir}`;
1714
+ });
1715
+ }
1716
+ /** List files in the project directory */
1717
+ async listProjectFiles() {
1718
+ const dir = await this.getProjectFilesDir();
1719
+ if (!dir) return [];
1720
+ try {
1721
+ const files = await readdir(dir);
1722
+ const items = await Promise.all(
1723
+ files.filter((file) => !file.startsWith(".")).map(async (file) => {
1724
+ const fileStat = await stat(join2(dir, file));
1725
+ return { name: file, size: fileStat.size, modified: fileStat.mtimeMs };
1726
+ })
1727
+ );
1728
+ return items;
1729
+ } catch {
1730
+ }
1731
+ return [];
1732
+ }
1733
+ /** Upload a file from project directory via filechooser (selector-based) */
1734
+ async parallelUploadFile(target, fileName, browserIds) {
1735
+ const dir = await this.getProjectFilesDir();
1736
+ if (!dir) {
1737
+ throw new Error("Project files directory not available");
1738
+ }
1739
+ return this.executeParallel(browserIds, async (page, browserId) => {
1740
+ const profile = this.getProfile(browserId);
1741
+ const selector = this.resolveSelector(target, browserId);
1742
+ const locator = page.locator(selector);
1743
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1744
+ await ensureCursor(page);
1745
+ await preActionDelay(profile);
1746
+ const fileChooserPromise = page.waitForEvent("filechooser", { timeout: 1e4 });
1747
+ const { target: clickTarget } = await getClickTarget(page, locator);
1748
+ const fromPos = this.getMousePos(browserId);
1749
+ await humanClick(page, clickTarget, fromPos, profile);
1750
+ this.setMousePos(browserId, clickTarget);
1751
+ const fileChooser = await fileChooserPromise;
1752
+ const filePath = join2(dir, fileName);
1753
+ await fileChooser.setFiles(filePath);
1754
+ return `Uploaded "${fileName}" from ${dir}`;
1755
+ }, { timeout: 15e3 });
1756
+ }
1757
+ // ==================== BATCH FORM FILL ====================
1758
+ async parallelFillForm(fields, browserIds) {
1759
+ return this.executeParallel(browserIds, async (page, browserId) => {
1760
+ const profile = this.getProfile(browserId);
1761
+ const filled = [];
1762
+ for (const field of fields) {
1763
+ const selector = this.resolveSelector(field, browserId);
1764
+ const locator = page.locator(selector);
1765
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1766
+ await ensureCursor(page);
1767
+ await preActionDelay(profile);
1768
+ if (field.type === "checkbox") {
1769
+ if (this.isTruthyFieldValue(field.value)) {
1770
+ await locator.check();
1771
+ } else {
1772
+ await locator.uncheck();
1773
+ }
1774
+ filled.push(`${selector}=${field.value}`);
1775
+ continue;
1776
+ }
1777
+ if (field.type === "radio") {
1778
+ await locator.check();
1779
+ filled.push(`${selector}=${field.value}`);
1780
+ continue;
1781
+ }
1782
+ if (field.type === "combobox") {
1783
+ const { target: clickTarget2 } = await getClickTarget(page, locator);
1784
+ const fromPos2 = this.getMousePos(browserId);
1785
+ await humanClick(page, clickTarget2, fromPos2, profile);
1786
+ this.setMousePos(browserId, clickTarget2);
1787
+ await humanDelay(120, 240);
1788
+ await locator.selectOption(field.value);
1789
+ filled.push(`${selector}="${field.value}"`);
1790
+ continue;
1791
+ }
1792
+ const { target: clickTarget } = await getClickTarget(page, locator);
1793
+ const fromPos = this.getMousePos(browserId);
1794
+ await humanClick(page, clickTarget, fromPos, profile);
1795
+ this.setMousePos(browserId, clickTarget);
1796
+ await humanDelay(80, 200);
1797
+ await page.keyboard.press("Meta+a");
1798
+ await humanDelay(30, 80);
1799
+ await page.keyboard.press("Backspace");
1800
+ await humanDelay(50, 150);
1801
+ await humanTypeText(page, field.value, profile);
1802
+ filled.push(`${selector}="${field.value}"`);
1803
+ }
1804
+ return `Filled ${filled.length} fields: ${filled.join(", ")}`;
1805
+ }, { timeout: fields.length * 5e3 + 5e3 });
1806
+ }
1807
+ // ==================== ELEMENT DRAG & DROP ====================
1808
+ async parallelDrag(startTarget, endTarget, browserIds) {
1809
+ return this.executeParallel(browserIds, async (page, browserId) => {
1810
+ const profile = this.getProfile(browserId);
1811
+ const startSelector = this.resolveSelector(startTarget, browserId);
1812
+ const endSelector = this.resolveSelector(endTarget, browserId);
1813
+ const startLocator = page.locator(startSelector);
1814
+ const endLocator = page.locator(endSelector);
1815
+ await startLocator.waitFor({ state: "visible", timeout: 5e3 });
1816
+ await endLocator.waitFor({ state: "visible", timeout: 5e3 });
1817
+ await ensureCursor(page);
1818
+ await preActionDelay(profile);
1819
+ const { target: startPos } = await getClickTarget(page, startLocator);
1820
+ const { target: endPos } = await getClickTarget(page, endLocator);
1821
+ const fromPos = this.getMousePos(browserId);
1822
+ await humanMouseMove(page, fromPos, startPos, profile);
1823
+ await humanDelay(50, 150);
1824
+ await page.mouse.down();
1825
+ await humanDelay(100, 250);
1826
+ await humanMouseMove(page, startPos, endPos, profile);
1827
+ await humanDelay(50, 150);
1828
+ await page.mouse.up();
1829
+ this.setMousePos(browserId, endPos);
1830
+ return `Dragged "${startSelector}" \u2192 "${endSelector}"`;
1831
+ }, { timeout: 15e3 });
1832
+ }
1833
+ // ==================== TAB MANAGEMENT ====================
1834
+ async parallelTabs(action, options = {}, browserIds) {
1835
+ return this.executeParallel(browserIds, async (page, browserId) => {
1836
+ const browser = this.browsers.get(browserId);
1837
+ if (!browser) throw new Error(`Browser ${browserId} not found`);
1838
+ const context = browser.context;
1839
+ const pages = context.pages();
1840
+ switch (action) {
1841
+ case "list": {
1842
+ return pages.map((p, i) => ({
1843
+ index: i,
1844
+ url: p.url(),
1845
+ active: p === browser.page
1846
+ }));
1847
+ }
1848
+ case "new": {
1849
+ const newPage = await context.newPage();
1850
+ if (options.url) {
1851
+ await newPage.goto(options.url, { waitUntil: "domcontentloaded" });
1852
+ }
1853
+ browser.page = newPage;
1854
+ newPage.setDefaultTimeout(this.defaultTimeout);
1855
+ newPage.setDefaultNavigationTimeout(6e4);
1856
+ this._setupPageTracking(browserId, newPage);
1857
+ return { index: context.pages().length - 1, url: newPage.url() };
1858
+ }
1859
+ case "close": {
1860
+ const idx = options.index ?? pages.indexOf(browser.page);
1861
+ if (idx < 0 || idx >= pages.length) throw new Error(`Tab index ${idx} out of range (0-${pages.length - 1})`);
1862
+ const targetPage = pages[idx];
1863
+ await targetPage.close();
1864
+ if (targetPage === browser.page) {
1865
+ const remaining = context.pages();
1866
+ if (remaining.length > 0) {
1867
+ browser.page = remaining[Math.min(idx, remaining.length - 1)];
1868
+ }
1869
+ }
1870
+ return { closed: idx, remaining: context.pages().length };
1871
+ }
1872
+ case "select": {
1873
+ if (options.index === void 0) throw new Error("index required for select action");
1874
+ const currentPages = context.pages();
1875
+ if (options.index < 0 || options.index >= currentPages.length) {
1876
+ throw new Error(`Tab index ${options.index} out of range (0-${currentPages.length - 1})`);
1877
+ }
1878
+ browser.page = currentPages[options.index];
1879
+ await browser.page.bringToFront();
1880
+ return { selected: options.index, url: browser.page.url() };
1881
+ }
1882
+ }
1883
+ }, { timeout: 3e4 });
1884
+ }
1885
+ /** Set up console/network tracking on a new page */
1886
+ _setupPageTracking(browserId, page) {
1887
+ const consoleLog = this.consoleMessages.get(browserId);
1888
+ if (consoleLog) {
1889
+ page.on("console", (msg) => {
1890
+ consoleLog.push({ type: msg.type(), text: msg.text(), timestamp: Date.now() });
1891
+ if (consoleLog.length > 1e3) consoleLog.shift();
1892
+ });
1893
+ }
1894
+ const networkLog = this.networkRequests.get(browserId);
1895
+ if (networkLog) {
1896
+ page.on("response", (response) => {
1897
+ const req = response.request();
1898
+ networkLog.push({
1899
+ url: req.url(),
1900
+ method: req.method(),
1901
+ status: response.status(),
1902
+ resourceType: req.resourceType(),
1903
+ timestamp: Date.now()
1904
+ });
1905
+ if (networkLog.length > 1e3) networkLog.shift();
1906
+ });
1907
+ }
1908
+ }
1909
+ // ==================== DIALOG HANDLING ====================
1910
+ async parallelHandleDialog(accept, promptText, browserIds) {
1911
+ return this.executeParallel(browserIds, async (page) => {
1912
+ return new Promise((resolve, reject) => {
1913
+ const timeout = setTimeout(() => {
1914
+ reject(new Error("No dialog appeared within 10 seconds"));
1915
+ }, 1e4);
1916
+ page.once("dialog", async (dialog) => {
1917
+ clearTimeout(timeout);
1918
+ const type = dialog.type();
1919
+ const message = dialog.message();
1920
+ try {
1921
+ if (accept) {
1922
+ await dialog.accept(promptText);
1923
+ } else {
1924
+ await dialog.dismiss();
1925
+ }
1926
+ resolve(`${accept ? "Accepted" : "Dismissed"} ${type} dialog: "${message}"`);
1927
+ } catch (e) {
1928
+ reject(e);
1929
+ }
1930
+ });
1931
+ });
1932
+ }, { timeout: 15e3 });
1933
+ }
1934
+ // ==================== CONSOLE MESSAGES ====================
1935
+ async parallelConsoleMessages(onlyErrors, browserIds) {
1936
+ await this.ensureBrowsersLoaded();
1937
+ const ids = browserIds ?? this.getBrowserIds();
1938
+ const results = ids.map((id) => {
1939
+ let messages = this.consoleMessages.get(id) || [];
1940
+ if (onlyErrors) {
1941
+ messages = messages.filter((m) => m.type === "error");
1942
+ }
1943
+ return {
1944
+ browserId: id,
1945
+ success: true,
1946
+ result: [...messages],
1947
+ duration: 0
1948
+ };
1949
+ });
1950
+ return {
1951
+ results,
1952
+ totalDuration: 0,
1953
+ successCount: results.length,
1954
+ failureCount: 0
1955
+ };
1956
+ }
1957
+ // ==================== NETWORK REQUESTS ====================
1958
+ async parallelNetworkRequests(browserIds) {
1959
+ await this.ensureBrowsersLoaded();
1960
+ const ids = browserIds ?? this.getBrowserIds();
1961
+ const results = ids.map((id) => {
1962
+ const requests = this.networkRequests.get(id) || [];
1963
+ return {
1964
+ browserId: id,
1965
+ success: true,
1966
+ result: [...requests],
1967
+ duration: 0
1968
+ };
1969
+ });
1970
+ return {
1971
+ results,
1972
+ totalDuration: 0,
1973
+ successCount: results.length,
1974
+ failureCount: 0
1975
+ };
1976
+ }
1977
+ /** Drag and drop a file onto a dropzone (selector-based) */
1978
+ async parallelDragDropFile(target, fileName, browserIds) {
1979
+ const dir = await this.getProjectFilesDir();
1980
+ if (!dir) {
1981
+ throw new Error("Project files directory not available");
1982
+ }
1983
+ return this.executeParallel(browserIds, async (page, browserId) => {
1984
+ const profile = this.getProfile(browserId);
1985
+ const selector = this.resolveSelector(target, browserId);
1986
+ const locator = page.locator(selector);
1987
+ await locator.waitFor({ state: "visible", timeout: 5e3 });
1988
+ await ensureCursor(page);
1989
+ await preActionDelay(profile);
1990
+ const { target: dropTarget } = await getClickTarget(page, locator);
1991
+ const fromPos = this.getMousePos(browserId);
1992
+ await humanMouseMove(page, fromPos, dropTarget, profile);
1993
+ this.setMousePos(browserId, dropTarget);
1994
+ const filePath = join2(dir, fileName);
1995
+ const fileBytes = Array.from(await readFile(filePath));
1996
+ const mimeType = this.getMimeType(fileName);
1997
+ await page.evaluate(async ({
1998
+ x,
1999
+ y,
2000
+ fileName: fileName2,
2001
+ mimeType: mimeType2,
2002
+ fileBytes: fileBytes2
2003
+ }) => {
2004
+ const dropzone = document.elementFromPoint(x, y);
2005
+ if (!dropzone) throw new Error("No element at drop coordinates");
2006
+ const dataTransfer = new DataTransfer();
2007
+ const file = new File([new Uint8Array(fileBytes2)], fileName2, { type: mimeType2 });
2008
+ dataTransfer.items.add(file);
2009
+ const events = ["dragenter", "dragover", "drop"];
2010
+ for (const eventType of events) {
2011
+ const event = new DragEvent(eventType, {
2012
+ bubbles: true,
2013
+ cancelable: true,
2014
+ dataTransfer
2015
+ });
2016
+ dropzone.dispatchEvent(event);
2017
+ await new Promise((r) => setTimeout(r, 50));
2018
+ }
2019
+ }, { x: dropTarget.x, y: dropTarget.y, fileName, mimeType, fileBytes });
2020
+ return `Dropped "${fileName}" onto element`;
2021
+ }, { timeout: 15e3 });
2022
+ }
2023
+ };
2024
+ }
2025
+ });
2026
+
2027
+ // client/index.ts
2028
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2029
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2030
+ import { readFileSync } from "node:fs";
2031
+ import { z } from "zod";
2032
+ import WebSocket from "ws";
2033
+ var args = process.argv.slice(2);
2034
+ var getArg = (name) => {
2035
+ const i = args.indexOf(`--${name}`);
2036
+ return i >= 0 ? args[i + 1] : void 0;
2037
+ };
2038
+ function getOptionalPort(argName, envName) {
2039
+ const raw = getArg(argName) ?? process.env[envName];
2040
+ if (raw === void 0 || raw === "") return void 0;
2041
+ const port = Number.parseInt(raw, 10);
2042
+ if (!Number.isFinite(port) || port <= 0) {
2043
+ console.error(`[ornold] Invalid --${argName}: ${raw}`);
2044
+ process.exit(1);
2045
+ }
2046
+ return port;
2047
+ }
2048
+ var TOKEN = getArg("token") || process.env.ORNOLD_TOKEN || "";
2049
+ var SERVER_URL = getArg("server") || process.env.ORNOLD_SERVER || "wss://ornold-mcp.fly.dev/bridge";
2050
+ var LINKEN_PORT = getOptionalPort("linken-port", "LINKEN_PORT");
2051
+ var DOLPHIN_PORT = getOptionalPort("dolphin-port", "DOLPHIN_PORT");
2052
+ var ENABLED_VENDORS = [
2053
+ ...LINKEN_PORT !== void 0 ? ["linken"] : [],
2054
+ ...DOLPHIN_PORT !== void 0 ? ["dolphin"] : []
2055
+ ];
2056
+ function getPackageVersion() {
2057
+ try {
2058
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
2059
+ return packageJson.version || "0.0.0";
2060
+ } catch {
2061
+ return "0.0.0";
2062
+ }
2063
+ }
2064
+ var CLIENT_VERSION = getPackageVersion();
2065
+ if (!TOKEN) {
2066
+ console.error("Usage: npx ornold-mcp --token <your_token>");
2067
+ console.error("Get your token at https://ornold-mcp.fly.dev");
2068
+ process.exit(1);
2069
+ }
2070
+ var ws = null;
2071
+ var serverReady = false;
2072
+ var pendingCalls = /* @__PURE__ */ new Map();
2073
+ var callCounter = 0;
2074
+ function connectServer() {
2075
+ return new Promise((resolve, reject) => {
2076
+ console.error(`[ornold] Connecting to ${SERVER_URL}...`);
2077
+ ws = new WebSocket(`${SERVER_URL}?token=${TOKEN}`);
2078
+ ws.on("open", () => {
2079
+ console.error("[ornold] Connected to server");
2080
+ });
2081
+ ws.on("message", (data) => {
2082
+ try {
2083
+ const msg = JSON.parse(data.toString());
2084
+ if (msg.type === "connected") {
2085
+ serverReady = true;
2086
+ console.error(`[ornold] Authenticated. Plan: ${msg.plan}, Profiles: ${msg.limits?.profiles}, Captchas: ${msg.limits?.captchas}`);
2087
+ resolve();
2088
+ }
2089
+ if (msg.type === "error") {
2090
+ console.error(`[ornold] Server error: ${msg.message}`);
2091
+ if (!serverReady) reject(new Error(msg.message));
2092
+ }
2093
+ if (msg.type === "tool_result" && msg.requestId) {
2094
+ const p = pendingCalls.get(msg.requestId);
2095
+ if (p) {
2096
+ clearTimeout(p.timer);
2097
+ pendingCalls.delete(msg.requestId);
2098
+ p.resolve(msg.result);
2099
+ }
2100
+ }
2101
+ if (msg.type === "execute") {
2102
+ handleExecuteCommand(msg).then((result) => {
2103
+ ws?.send(JSON.stringify({
2104
+ type: "execute_result",
2105
+ requestId: msg.requestId,
2106
+ result
2107
+ }));
2108
+ });
2109
+ }
2110
+ } catch {
2111
+ }
2112
+ });
2113
+ ws.on("close", (code) => {
2114
+ serverReady = false;
2115
+ if (code === 4001) {
2116
+ console.error("[ornold] Invalid token. Get your token at https://ornold-mcp.fly.dev");
2117
+ process.exit(1);
2118
+ }
2119
+ console.error(`[ornold] Disconnected (${code}). Reconnecting in 3s...`);
2120
+ setTimeout(connectServer, 3e3);
2121
+ });
2122
+ ws.on("error", (err) => {
2123
+ console.error(`[ornold] WS error: ${err.message}`);
2124
+ });
2125
+ });
2126
+ }
2127
+ function callServer(tool, toolArgs) {
2128
+ if (!ws || ws.readyState !== WebSocket.OPEN || !serverReady) {
2129
+ return Promise.resolve({ content: "Not connected to Ornold server. Check your token and internet connection.", isError: true });
2130
+ }
2131
+ const requestId = `r_${++callCounter}`;
2132
+ return new Promise((resolve) => {
2133
+ const timer = setTimeout(() => {
2134
+ pendingCalls.delete(requestId);
2135
+ resolve({ content: "Server timeout", isError: true });
2136
+ }, 12e4);
2137
+ pendingCalls.set(requestId, { resolve, timer });
2138
+ ws.send(JSON.stringify({
2139
+ type: "tool_call",
2140
+ requestId,
2141
+ tool,
2142
+ args: toolArgs
2143
+ }));
2144
+ });
2145
+ }
2146
+ var executor = null;
2147
+ async function getExecutor() {
2148
+ if (!executor) {
2149
+ const { MultiBrowserExecutor: MultiBrowserExecutor2 } = await Promise.resolve().then(() => (init_multi_browser(), multi_browser_exports));
2150
+ executor = new MultiBrowserExecutor2();
2151
+ }
2152
+ return executor;
2153
+ }
2154
+ function getSelectorTarget(params) {
2155
+ const selector = typeof params.selector === "string" ? params.selector : void 0;
2156
+ const ref = typeof params.ref === "string" || typeof params.ref === "number" ? String(params.ref) : void 0;
2157
+ if (!selector && !ref) {
2158
+ throw new Error("Either selector or ref is required");
2159
+ }
2160
+ return selector ? { selector } : { ref };
2161
+ }
2162
+ async function handleExecuteCommand(msg) {
2163
+ const { command, params } = msg;
2164
+ try {
2165
+ switch (command) {
2166
+ // CDP commands — executed via local Patchright
2167
+ case "cdp_status": {
2168
+ const exec = await getExecutor();
2169
+ await exec.ensureBrowsersLoaded();
2170
+ const status = await exec.getDetailedStatus({ checkContent: !!params.checkContent });
2171
+ return { content: JSON.stringify(status), isError: false };
2172
+ }
2173
+ case "cdp_tabs": {
2174
+ const exec = await getExecutor();
2175
+ const r = await exec.parallelTabs(params.action, { index: params.index, url: params.url }, params.browserIds);
2176
+ return { content: formatResult(r), isError: false };
2177
+ }
2178
+ case "cdp_navigate": {
2179
+ const exec = await getExecutor();
2180
+ const r = await exec.parallelNavigate(params.url, params.browserIds);
2181
+ return { content: formatResult(r), isError: false };
2182
+ }
2183
+ case "cdp_navigate_multi": {
2184
+ const exec = await getExecutor();
2185
+ const r = await exec.parallelNavigateMulti(params.targets);
2186
+ return { content: formatResult(r), isError: false };
2187
+ }
2188
+ case "cdp_click": {
2189
+ const exec = await getExecutor();
2190
+ const r = await exec.parallelClick(getSelectorTarget(params), params.browserIds);
2191
+ return { content: formatResult(r), isError: false };
2192
+ }
2193
+ case "cdp_type": {
2194
+ const exec = await getExecutor();
2195
+ const r = await exec.parallelType(getSelectorTarget(params), params.text, params.browserIds);
2196
+ return { content: formatResult(r), isError: false };
2197
+ }
2198
+ case "cdp_fill": {
2199
+ const exec = await getExecutor();
2200
+ const target = getSelectorTarget(params);
2201
+ const r = params.texts ? await exec.parallelFillMulti(target, params.texts) : await exec.parallelFill(target, params.text, params.browserIds);
2202
+ return { content: formatResult(r), isError: false };
2203
+ }
2204
+ case "cdp_fill_multi": {
2205
+ const exec = await getExecutor();
2206
+ const r = await exec.parallelFillMulti(getSelectorTarget(params), params.texts);
2207
+ return { content: formatResult(r), isError: false };
2208
+ }
2209
+ case "cdp_fill_form": {
2210
+ const exec = await getExecutor();
2211
+ const r = await exec.parallelFillForm(params.fields, params.browserIds);
2212
+ return { content: formatResult(r), isError: false };
2213
+ }
2214
+ case "cdp_drag": {
2215
+ const exec = await getExecutor();
2216
+ const r = await exec.parallelDrag(
2217
+ {
2218
+ selector: typeof params.startSelector === "string" ? params.startSelector : void 0,
2219
+ ref: typeof params.startRef === "string" || typeof params.startRef === "number" ? String(params.startRef) : void 0
2220
+ },
2221
+ {
2222
+ selector: typeof params.endSelector === "string" ? params.endSelector : void 0,
2223
+ ref: typeof params.endRef === "string" || typeof params.endRef === "number" ? String(params.endRef) : void 0
2224
+ },
2225
+ params.browserIds
2226
+ );
2227
+ return { content: formatResult(r), isError: false };
2228
+ }
2229
+ case "cdp_snapshot": {
2230
+ const exec = await getExecutor();
2231
+ const r = await exec.parallelSnapshot(params.browserIds, { compact: params.compact });
2232
+ const parts = (r.results || []).map((item) => {
2233
+ if (!item.success) return `[${item.browserId}] ERROR: ${item.error || "unknown"}`;
2234
+ const result = item.result || {};
2235
+ return `### ${item.browserId}
2236
+ URL: ${result.url || ""}
2237
+ Title: ${result.title || ""}
2238
+ ${result.snapshot || ""}`;
2239
+ });
2240
+ return { content: parts.join("\n\n") || "OK", isError: false };
2241
+ }
2242
+ case "cdp_screenshot": {
2243
+ const exec = await getExecutor();
2244
+ const r = await exec.parallelScreenshot(params.browserIds, { fullPage: params.fullPage });
2245
+ const lines = (r.results || []).map((item) => {
2246
+ if (!item.success) return `[${item.browserId}] ERROR: ${item.error || "unknown"}`;
2247
+ const image = Buffer.isBuffer(item.result) ? item.result.toString("base64") : Buffer.from(item.result || []).toString("base64");
2248
+ return `[${item.browserId}] ${image}`;
2249
+ });
2250
+ return { content: lines.join("\n") || "OK", isError: false };
2251
+ }
2252
+ case "cdp_evaluate": {
2253
+ const exec = await getExecutor();
2254
+ const r = await exec.parallelEvaluate(params.script, params.browserIds);
2255
+ return { content: formatResult(r), isError: false };
2256
+ }
2257
+ case "cdp_run_code": {
2258
+ const exec = await getExecutor();
2259
+ const r = await exec.parallelRunCode(params.code, params.browserIds);
2260
+ return { content: formatResult(r), isError: false };
2261
+ }
2262
+ case "cdp_run_code_with_vars": {
2263
+ const exec = await getExecutor();
2264
+ const r = await exec.parallelRunCodeWithVariables(params.codeTemplate, params.variables);
2265
+ return { content: formatResult(r), isError: false };
2266
+ }
2267
+ case "cdp_press_key": {
2268
+ const exec = await getExecutor();
2269
+ const r = await exec.parallelPressKey(params.key, params.browserIds);
2270
+ return { content: formatResult(r), isError: false };
2271
+ }
2272
+ case "cdp_select_option": {
2273
+ const exec = await getExecutor();
2274
+ const r = await exec.parallelSelectOption(getSelectorTarget(params), params.values, params.browserIds);
2275
+ return { content: formatResult(r), isError: false };
2276
+ }
2277
+ case "cdp_hover": {
2278
+ const exec = await getExecutor();
2279
+ const r = await exec.parallelHover(getSelectorTarget(params), params.browserIds);
2280
+ return { content: formatResult(r), isError: false };
2281
+ }
2282
+ case "cdp_go_back": {
2283
+ const exec = await getExecutor();
2284
+ const r = await exec.parallelGoBack(params.browserIds);
2285
+ return { content: formatResult(r), isError: false };
2286
+ }
2287
+ case "cdp_go_forward": {
2288
+ const exec = await getExecutor();
2289
+ const r = await exec.parallelGoForward(params.browserIds);
2290
+ return { content: formatResult(r), isError: false };
2291
+ }
2292
+ case "cdp_reload": {
2293
+ const exec = await getExecutor();
2294
+ const r = await exec.parallelReload(params.browserIds);
2295
+ return { content: formatResult(r), isError: false };
2296
+ }
2297
+ case "cdp_wait_for": {
2298
+ const exec = await getExecutor();
2299
+ const opts = {};
2300
+ if (params.time) opts.time = params.time;
2301
+ if (params.text) opts.text = params.text;
2302
+ if (params.textGone) opts.textGone = params.textGone;
2303
+ if (params.domStableMs) opts.domStableMs = params.domStableMs;
2304
+ if (params.timeoutMs) opts.timeoutMs = params.timeoutMs;
2305
+ const r = await exec.parallelWaitFor(opts, params.browserIds);
2306
+ return { content: formatResult(r), isError: false };
2307
+ }
2308
+ case "cdp_handle_dialog": {
2309
+ const exec = await getExecutor();
2310
+ const r = await exec.parallelHandleDialog(!!params.accept, params.promptText, params.browserIds);
2311
+ return { content: formatResult(r), isError: false };
2312
+ }
2313
+ case "cdp_console_messages": {
2314
+ const exec = await getExecutor();
2315
+ const r = await exec.parallelConsoleMessages(!!params.onlyErrors, params.browserIds);
2316
+ return { content: formatResult(r), isError: false };
2317
+ }
2318
+ case "cdp_network_requests": {
2319
+ const exec = await getExecutor();
2320
+ const r = await exec.parallelNetworkRequests(params.browserIds);
2321
+ return { content: formatResult(r), isError: false };
2322
+ }
2323
+ case "cdp_click_normalized_box": {
2324
+ const exec = await getExecutor();
2325
+ const r = await exec.parallelClickNormalizedBox(params.box, params.browserIds);
2326
+ return { content: formatResult(r), isError: false };
2327
+ }
2328
+ case "cdp_setup_downloads": {
2329
+ const exec = await getExecutor();
2330
+ const r = await exec.setupDownloads(params.browserIds);
2331
+ return { content: formatResult(r), isError: false };
2332
+ }
2333
+ case "cdp_list_downloads": {
2334
+ const exec = await getExecutor();
2335
+ const files = await exec.listProjectFiles();
2336
+ return { content: JSON.stringify(files), isError: false };
2337
+ }
2338
+ case "cdp_upload_file": {
2339
+ const exec = await getExecutor();
2340
+ const r = await exec.parallelUploadFile(getSelectorTarget(params), params.fileName, params.browserIds);
2341
+ return { content: formatResult(r), isError: false };
2342
+ }
2343
+ case "cdp_drag_drop_file": {
2344
+ const exec = await getExecutor();
2345
+ const r = await exec.parallelDragDropFile(getSelectorTarget(params), params.fileName, params.browserIds);
2346
+ return { content: formatResult(r), isError: false };
2347
+ }
2348
+ case "cdp_vision_capture": {
2349
+ const exec = await getExecutor();
2350
+ await exec.ensureBrowsersLoaded();
2351
+ const statuses = await exec.listBrowsers();
2352
+ const statusMap = new Map(statuses.map((browser) => [browser.id, browser]));
2353
+ const screenshotResult = await exec.parallelScreenshot(params.browserIds, {
2354
+ format: "jpeg",
2355
+ quality: 70
2356
+ });
2357
+ const captures = (screenshotResult.results || []).filter((item) => item.success && item.result !== void 0).map((item) => ({
2358
+ browserId: item.browserId,
2359
+ url: statusMap.get(item.browserId)?.url || "",
2360
+ screenshot: Buffer.isBuffer(item.result) ? item.result.toString("base64") : Buffer.from(item.result || []).toString("base64")
2361
+ }));
2362
+ const errors = (screenshotResult.results || []).filter((item) => !item.success).map((item) => ({
2363
+ browserId: item.browserId,
2364
+ error: item.error || "unknown"
2365
+ }));
2366
+ return { content: JSON.stringify({ captures, errors }), isError: captures.length === 0 };
2367
+ }
2368
+ case "cdp_list_browsers": {
2369
+ const exec = await getExecutor();
2370
+ await exec.ensureBrowsersLoaded();
2371
+ const browsers = await exec.listBrowsers();
2372
+ return { content: browsers.length === 0 ? "No browsers connected" : browsers.map((b) => `${b.id} ${b.connected ? "OK" : "DISCONNECTED"} ${b.url || ""}`).join("\n"), isError: false };
2373
+ }
2374
+ case "cdp_connect": {
2375
+ const exec = await getExecutor();
2376
+ await exec.connect({ id: params.id, cdpEndpoint: params.cdpEndpoint });
2377
+ return { content: `Connected: ${params.id}`, isError: false };
2378
+ }
2379
+ case "cdp_disconnect": {
2380
+ const exec = await getExecutor();
2381
+ await exec.disconnect(params.id);
2382
+ return { content: `Disconnected: ${params.id}`, isError: false };
2383
+ }
2384
+ // Press and hold (for captcha solving)
2385
+ case "cdp_press_hold": {
2386
+ const exec = await getExecutor();
2387
+ const browsers = params.browserIds ? void 0 : await exec.listBrowsers();
2388
+ const targetId = params.browserIds?.[0] || browsers?.[0]?.id;
2389
+ if (!targetId) return { content: "No browser connected", isError: true };
2390
+ const page = exec.getPage(targetId);
2391
+ if (!page) return { content: "Browser page not found", isError: true };
2392
+ const x = params.x;
2393
+ const y = params.y;
2394
+ const duration = params.duration;
2395
+ await page.mouse.move(x, y);
2396
+ await page.mouse.down();
2397
+ const start = Date.now();
2398
+ while (Date.now() - start < duration) {
2399
+ await page.waitForTimeout(80 + Math.random() * 170);
2400
+ await page.mouse.move(x + (Math.random() - 0.5) * 6, y + (Math.random() - 0.5) * 6);
2401
+ }
2402
+ await page.mouse.up();
2403
+ return { content: JSON.stringify({ held: true, duration }), isError: false };
2404
+ }
2405
+ // Antidetect HTTP proxy — executed locally
2406
+ case "http_request": {
2407
+ const resp = await fetch(params.url, {
2408
+ method: params.method || "GET",
2409
+ headers: params.headers || { "Content-Type": "application/json" },
2410
+ body: params.body ? JSON.stringify(params.body) : void 0,
2411
+ signal: AbortSignal.timeout(3e4)
2412
+ });
2413
+ const data = await resp.text();
2414
+ try {
2415
+ return { content: JSON.stringify(JSON.parse(data)), isError: !resp.ok };
2416
+ } catch {
2417
+ return { content: data, isError: !resp.ok };
2418
+ }
2419
+ }
2420
+ default:
2421
+ return { content: `Unknown command: ${command}`, isError: true };
2422
+ }
2423
+ } catch (e) {
2424
+ return { content: `Execution error: ${e.message}`, isError: true };
2425
+ }
2426
+ }
2427
+ function formatResult(r) {
2428
+ const lines = [];
2429
+ for (const item of r.results || []) {
2430
+ if (item.success && item.result !== void 0) {
2431
+ const val = typeof item.result === "string" ? item.result : JSON.stringify(item.result);
2432
+ lines.push(`[${item.browserId}] ${val.slice(0, 4e3)}`);
2433
+ } else if (!item.success) {
2434
+ lines.push(`[${item.browserId}] ERROR: ${item.error || "unknown"}`);
2435
+ }
2436
+ }
2437
+ return lines.join("\n") || "OK";
2438
+ }
2439
+ function textResult(text, isError = false) {
2440
+ return { content: [{ type: "text", text }], isError };
2441
+ }
2442
+ async function toolHandler(tool, args2) {
2443
+ const result = await callServer(tool, args2);
2444
+ return textResult(result.content, result.isError);
2445
+ }
2446
+ function registerLinkenTools(server) {
2447
+ server.tool("linken_get_instances", "Get list of Linken Sphere sessions", {
2448
+ status: z.enum(["stopped", "running", "imported", "warmup", "automationRunning"]).optional(),
2449
+ proxy_info: z.boolean().optional()
2450
+ }, (args2) => toolHandler("linken_get_instances", args2));
2451
+ server.tool("linken_create_quick_sessions", "Create Linken Sphere sessions", {
2452
+ count: z.number().optional()
2453
+ }, (args2) => toolHandler("linken_create_quick_sessions", args2));
2454
+ server.tool("linken_start_instances", "Start Linken browser sessions", {
2455
+ uuids: z.array(z.string()),
2456
+ headless: z.boolean().optional(),
2457
+ debug_port: z.number().optional(),
2458
+ _scope: z.string().optional()
2459
+ }, (args2) => toolHandler("linken_start_instances", args2));
2460
+ server.tool("linken_stop_instances", "Stop Linken sessions", {
2461
+ uuids: z.array(z.string())
2462
+ }, (args2) => toolHandler("linken_stop_instances", args2));
2463
+ server.tool("linken_session_info", "Get session details", {
2464
+ uuid: z.string()
2465
+ }, (args2) => toolHandler("linken_session_info", args2));
2466
+ server.tool("linken_force_stop", "Force stop Linken session", {
2467
+ uuid: z.string()
2468
+ }, (args2) => toolHandler("linken_force_stop", args2));
2469
+ server.tool("linken_delete_sessions", "Delete Linken sessions", {
2470
+ uuid: z.string().optional(),
2471
+ uuids: z.array(z.string()).optional()
2472
+ }, (args2) => toolHandler("linken_delete_sessions", args2));
2473
+ server.tool("linken_unlock_stopped_sessions", "Unlock stopped Linken sessions", {}, () => toolHandler("linken_unlock_stopped_sessions", {}));
2474
+ server.tool("linken_set_session_name", "Rename Linken session", {
2475
+ uuid: z.string(),
2476
+ name: z.string()
2477
+ }, (args2) => toolHandler("linken_set_session_name", args2));
2478
+ server.tool("linken_set_connection", "Set proxy for Linken session", {
2479
+ uuid: z.string(),
2480
+ type: z.string(),
2481
+ ip: z.string().optional(),
2482
+ port: z.number().optional(),
2483
+ login: z.string().optional(),
2484
+ password: z.string().optional()
2485
+ }, (args2) => toolHandler("linken_set_connection", args2));
2486
+ server.tool("linken_check_connection", "Check proxy for Linken session", {
2487
+ uuid: z.string()
2488
+ }, (args2) => toolHandler("linken_check_connection", args2));
2489
+ server.tool("linken_set_geo", "Set Linken geolocation", {
2490
+ uuid: z.string(),
2491
+ timezone: z.string().optional(),
2492
+ language: z.string().optional(),
2493
+ latitude: z.number().optional(),
2494
+ longitude: z.number().optional()
2495
+ }, (args2) => toolHandler("linken_set_geo", args2));
2496
+ server.tool("linken_set_useragent", "Set Linken user agent", {
2497
+ uuid: z.string(),
2498
+ useragent: z.string()
2499
+ }, (args2) => toolHandler("linken_set_useragent", args2));
2500
+ server.tool("linken_import_cookies", "Import cookies into Linken session", {
2501
+ uuid: z.string(),
2502
+ file_path: z.string().optional(),
2503
+ json: z.string().optional()
2504
+ }, (args2) => toolHandler("linken_import_cookies", args2));
2505
+ server.tool("linken_export_cookies", "Export cookies from Linken sessions", {
2506
+ uuids: z.array(z.string()),
2507
+ folder_path: z.string()
2508
+ }, (args2) => toolHandler("linken_export_cookies", args2));
2509
+ server.tool("linken_start_warmup", "Start Linken session warmup", {
2510
+ uuid: z.string(),
2511
+ url_count: z.number().optional(),
2512
+ time_per_url: z.number().optional(),
2513
+ view_depth: z.number().optional(),
2514
+ urls: z.array(z.string()).optional()
2515
+ }, (args2) => toolHandler("linken_start_warmup", args2));
2516
+ server.tool("linken_get_providers", "List Linken providers", {}, () => toolHandler("linken_get_providers", {}));
2517
+ server.tool("linken_set_active_provider", "Set active Linken provider", {
2518
+ uuid: z.string()
2519
+ }, (args2) => toolHandler("linken_set_active_provider", args2));
2520
+ server.tool("linken_delete_provider", "Delete Linken provider", {
2521
+ uuid: z.string()
2522
+ }, (args2) => toolHandler("linken_delete_provider", args2));
2523
+ server.tool("linken_get_desktops", "List Linken desktops", {}, () => toolHandler("linken_get_desktops", {}));
2524
+ server.tool("linken_create_desktop", "Create Linken desktop", {
2525
+ name: z.string(),
2526
+ team_uuid: z.string().optional()
2527
+ }, (args2) => toolHandler("linken_create_desktop", args2));
2528
+ server.tool("linken_set_active_desktop", "Set active Linken desktop", {
2529
+ uuid: z.string()
2530
+ }, (args2) => toolHandler("linken_set_active_desktop", args2));
2531
+ server.tool("linken_delete_desktop", "Delete Linken desktop", {
2532
+ uuid: z.string()
2533
+ }, (args2) => toolHandler("linken_delete_desktop", args2));
2534
+ server.tool("linken_optimize_storage", "Optimize Linken storage", {}, () => toolHandler("linken_optimize_storage", {}));
2535
+ }
2536
+ var browserIdsArg = z.array(z.string()).optional();
2537
+ var browserScopeArg = z.string().optional();
2538
+ var browserRefArg = z.union([z.string(), z.number()]).optional();
2539
+ var browserTargetArgs = {
2540
+ element: z.string().optional(),
2541
+ ref: browserRefArg,
2542
+ selector: z.string().optional(),
2543
+ browserIds: browserIdsArg,
2544
+ _scope: browserScopeArg
2545
+ };
2546
+ function createServer() {
2547
+ const server = new McpServer({ name: "ornold-browser", version: CLIENT_VERSION });
2548
+ server.tool("browser_list", "List connected browsers", {}, () => toolHandler("browser_list", {}));
2549
+ server.tool("browser_status", "Check browser sync and responsiveness", {
2550
+ checkContent: z.boolean().optional(),
2551
+ _scope: browserScopeArg
2552
+ }, (args2) => toolHandler("browser_status", args2));
2553
+ server.tool("browser_parallel_tabs", "Manage browser tabs in parallel", {
2554
+ action: z.enum(["list", "new", "close", "select"]),
2555
+ index: z.number().optional(),
2556
+ url: z.string().optional(),
2557
+ browserIds: browserIdsArg,
2558
+ _scope: browserScopeArg
2559
+ }, (args2) => toolHandler("browser_parallel_tabs", args2));
2560
+ server.tool("browser_parallel_snapshot", "Get page snapshot with [ref=N] markers", {
2561
+ compact: z.boolean().optional(),
2562
+ browserIds: browserIdsArg,
2563
+ _scope: browserScopeArg
2564
+ }, (args2) => toolHandler("browser_parallel_snapshot", args2));
2565
+ server.tool("browser_parallel_navigate", "Navigate to URL", {
2566
+ url: z.string(),
2567
+ browserIds: browserIdsArg,
2568
+ _scope: browserScopeArg
2569
+ }, (args2) => toolHandler("browser_parallel_navigate", args2));
2570
+ server.tool("browser_parallel_navigate_multi", "Navigate each browser to a different URL", {
2571
+ targets: z.array(z.object({
2572
+ browserId: z.string(),
2573
+ url: z.string()
2574
+ })),
2575
+ _scope: browserScopeArg
2576
+ }, (args2) => toolHandler("browser_parallel_navigate_multi", args2));
2577
+ server.tool("browser_parallel_click", "Click element", {
2578
+ ...browserTargetArgs
2579
+ }, (args2) => toolHandler("browser_parallel_click", args2));
2580
+ server.tool("browser_parallel_type", "Type text into element", {
2581
+ ...browserTargetArgs,
2582
+ text: z.string()
2583
+ }, (args2) => toolHandler("browser_parallel_type", args2));
2584
+ server.tool("browser_parallel_fill", "Fill input (clear + type)", {
2585
+ ...browserTargetArgs,
2586
+ text: z.string().optional(),
2587
+ texts: z.record(z.string()).optional()
2588
+ }, (args2) => toolHandler("browser_parallel_fill", args2));
2589
+ server.tool("browser_parallel_fill_multi", "Fill input with per-browser values", {
2590
+ ...browserTargetArgs,
2591
+ texts: z.record(z.string())
2592
+ }, (args2) => toolHandler("browser_parallel_fill_multi", args2));
2593
+ server.tool("browser_parallel_fill_form", "Fill multiple form fields sequentially", {
2594
+ fields: z.array(z.object({
2595
+ element: z.string().optional(),
2596
+ ref: browserRefArg,
2597
+ selector: z.string().optional(),
2598
+ value: z.string(),
2599
+ type: z.enum(["textbox", "checkbox", "radio", "combobox"]).optional()
2600
+ })),
2601
+ browserIds: browserIdsArg,
2602
+ _scope: browserScopeArg
2603
+ }, (args2) => toolHandler("browser_parallel_fill_form", args2));
2604
+ server.tool("browser_parallel_drag", "Drag from one element to another", {
2605
+ startElement: z.string().optional(),
2606
+ startRef: browserRefArg,
2607
+ startSelector: z.string().optional(),
2608
+ endElement: z.string().optional(),
2609
+ endRef: browserRefArg,
2610
+ endSelector: z.string().optional(),
2611
+ browserIds: browserIdsArg,
2612
+ _scope: browserScopeArg
2613
+ }, (args2) => toolHandler("browser_parallel_drag", args2));
2614
+ server.tool("browser_parallel_press_key", "Press keyboard key", {
2615
+ key: z.string(),
2616
+ browserIds: browserIdsArg,
2617
+ _scope: browserScopeArg
2618
+ }, (args2) => toolHandler("browser_parallel_press_key", args2));
2619
+ server.tool("browser_parallel_select_option", "Select dropdown option", {
2620
+ ...browserTargetArgs,
2621
+ values: z.array(z.string())
2622
+ }, (args2) => toolHandler("browser_parallel_select_option", args2));
2623
+ server.tool("browser_parallel_wait_for", "Wait for condition", {
2624
+ time: z.number().optional(),
2625
+ text: z.string().optional(),
2626
+ textGone: z.string().optional(),
2627
+ domStableMs: z.number().optional(),
2628
+ timeoutMs: z.number().optional(),
2629
+ browserIds: browserIdsArg,
2630
+ _scope: browserScopeArg
2631
+ }, (args2) => toolHandler("browser_parallel_wait_for", args2));
2632
+ server.tool("browser_parallel_screenshot", "Take screenshot", {
2633
+ fullPage: z.boolean().optional(),
2634
+ browserIds: browserIdsArg,
2635
+ _scope: browserScopeArg
2636
+ }, (args2) => toolHandler("browser_parallel_screenshot", args2));
2637
+ server.tool("browser_parallel_evaluate", "Run JavaScript in page", {
2638
+ script: z.string(),
2639
+ browserIds: browserIdsArg,
2640
+ _scope: browserScopeArg
2641
+ }, (args2) => toolHandler("browser_parallel_evaluate", args2));
2642
+ server.tool("browser_parallel_run_code", "Run JavaScript code in page context", {
2643
+ code: z.string(),
2644
+ browserIds: browserIdsArg,
2645
+ _scope: browserScopeArg
2646
+ }, (args2) => toolHandler("browser_parallel_run_code", args2));
2647
+ server.tool("browser_parallel_run_code_with_vars", "Run templated JavaScript code with per-browser variables", {
2648
+ codeTemplate: z.string(),
2649
+ variables: z.record(z.record(z.string())),
2650
+ _scope: browserScopeArg
2651
+ }, (args2) => toolHandler("browser_parallel_run_code_with_vars", args2));
2652
+ server.tool("browser_parallel_hover", "Hover over element", {
2653
+ ...browserTargetArgs
2654
+ }, (args2) => toolHandler("browser_parallel_hover", args2));
2655
+ server.tool("browser_parallel_go_back", "Go back", {
2656
+ browserIds: browserIdsArg,
2657
+ _scope: browserScopeArg
2658
+ }, (args2) => toolHandler("browser_parallel_go_back", args2));
2659
+ server.tool("browser_parallel_go_forward", "Go forward", {
2660
+ browserIds: browserIdsArg,
2661
+ _scope: browserScopeArg
2662
+ }, (args2) => toolHandler("browser_parallel_go_forward", args2));
2663
+ server.tool("browser_parallel_reload", "Reload page", {
2664
+ browserIds: browserIdsArg,
2665
+ _scope: browserScopeArg
2666
+ }, (args2) => toolHandler("browser_parallel_reload", args2));
2667
+ server.tool("browser_parallel_handle_dialog", "Handle JavaScript dialogs", {
2668
+ accept: z.boolean(),
2669
+ promptText: z.string().optional(),
2670
+ browserIds: browserIdsArg,
2671
+ _scope: browserScopeArg
2672
+ }, (args2) => toolHandler("browser_parallel_handle_dialog", args2));
2673
+ server.tool("browser_parallel_console_messages", "Get console messages", {
2674
+ onlyErrors: z.boolean().optional(),
2675
+ browserIds: browserIdsArg,
2676
+ _scope: browserScopeArg
2677
+ }, (args2) => toolHandler("browser_parallel_console_messages", args2));
2678
+ server.tool("browser_parallel_network_requests", "Get network requests", {
2679
+ browserIds: browserIdsArg,
2680
+ _scope: browserScopeArg
2681
+ }, (args2) => toolHandler("browser_parallel_network_requests", args2));
2682
+ server.tool("browser_parallel_vision_analyze_grouped", "Analyze grouped browser screenshots with OmniParser via Ornold server", {
2683
+ similarityThreshold: z.number().optional(),
2684
+ browserIds: browserIdsArg,
2685
+ _scope: browserScopeArg
2686
+ }, (args2) => toolHandler("browser_parallel_vision_analyze_grouped", args2));
2687
+ server.tool("browser_parallel_click_normalized_box", "Click normalized viewport box center", {
2688
+ box: z.tuple([z.number(), z.number(), z.number(), z.number()]),
2689
+ browserIds: browserIdsArg,
2690
+ _scope: browserScopeArg
2691
+ }, (args2) => toolHandler("browser_parallel_click_normalized_box", args2));
2692
+ server.tool("browser_setup_downloads", "Enable browser downloads to project files directory", {
2693
+ browserIds: browserIdsArg,
2694
+ _scope: browserScopeArg
2695
+ }, (args2) => toolHandler("browser_setup_downloads", args2));
2696
+ server.tool("browser_list_downloads", "List files in project files directory", {}, () => toolHandler("browser_list_downloads", {}));
2697
+ server.tool("browser_parallel_upload_file", "Upload a file to an input element", {
2698
+ ...browserTargetArgs,
2699
+ fileName: z.string()
2700
+ }, (args2) => toolHandler("browser_parallel_upload_file", args2));
2701
+ server.tool("browser_parallel_drag_drop_file", "Drag and drop a file onto an element", {
2702
+ ...browserTargetArgs,
2703
+ fileName: z.string()
2704
+ }, (args2) => toolHandler("browser_parallel_drag_drop_file", args2));
2705
+ server.tool("browser_search_macros", "Search saved browser macros", {
2706
+ name: z.string(),
2707
+ _scope: browserScopeArg
2708
+ }, (args2) => toolHandler("browser_search_macros", args2));
2709
+ server.tool("browser_run_recorded_flow", "Run saved browser macro by ID", {
2710
+ macroId: z.string(),
2711
+ browserIds: browserIdsArg,
2712
+ _scope: browserScopeArg
2713
+ }, (args2) => toolHandler("browser_run_recorded_flow", args2));
2714
+ if (LINKEN_PORT !== void 0) {
2715
+ registerLinkenTools(server);
2716
+ }
2717
+ if (DOLPHIN_PORT !== void 0) {
2718
+ server.tool("dolphin_get_profiles", "List Dolphin profiles", {}, () => toolHandler("dolphin_get_profiles", {}));
2719
+ server.tool("dolphin_start_profile", "Start Dolphin profile", {
2720
+ profileId: z.string()
2721
+ }, (args2) => toolHandler("dolphin_start_profile", args2));
2722
+ server.tool("dolphin_stop_profile", "Stop Dolphin profile", {
2723
+ profileId: z.string()
2724
+ }, (args2) => toolHandler("dolphin_stop_profile", args2));
2725
+ }
2726
+ server.tool("browser_detect_captcha", "Detect captcha on page", {
2727
+ browserIds: browserIdsArg,
2728
+ _scope: browserScopeArg
2729
+ }, (args2) => toolHandler("browser_detect_captcha", args2));
2730
+ server.tool("browser_solve_press_hold", "Solve press-and-hold captcha", {
2731
+ browserIds: browserIdsArg,
2732
+ maxAttempts: z.number().optional(),
2733
+ holdMs: z.number().optional(),
2734
+ _scope: browserScopeArg
2735
+ }, (args2) => toolHandler("browser_solve_press_hold", args2));
2736
+ server.tool("browser_detect_press_hold", "Detect press-and-hold captcha", {
2737
+ browserIds: browserIdsArg,
2738
+ _scope: browserScopeArg
2739
+ }, (args2) => toolHandler("browser_detect_press_hold", args2));
2740
+ server.tool("browser_solve_captcha", "Solve captcha (reCAPTCHA/hCaptcha)", {
2741
+ browserIds: browserIdsArg,
2742
+ timeout: z.number().optional(),
2743
+ autoSubmit: z.boolean().optional(),
2744
+ _scope: browserScopeArg
2745
+ }, (args2) => toolHandler("browser_solve_captcha", args2));
2746
+ server.tool("browser_captcha_balance", "Check 2captcha balance", {}, () => toolHandler("browser_captcha_balance", {}));
2747
+ server.tool("captcha_detect", "Detect captcha on page", {
2748
+ browserIds: browserIdsArg
2749
+ }, (args2) => toolHandler("captcha_detect", args2));
2750
+ server.tool("captcha_solve_press_hold", "Solve press-and-hold captcha", {
2751
+ browserIds: browserIdsArg
2752
+ }, (args2) => toolHandler("captcha_solve_press_hold", args2));
2753
+ server.tool("captcha_solve", "Solve captcha (reCAPTCHA/hCaptcha)", {
2754
+ browserIds: browserIdsArg
2755
+ }, (args2) => toolHandler("captcha_solve", args2));
2756
+ return server;
2757
+ }
2758
+ async function main() {
2759
+ await connectServer();
2760
+ if (ENABLED_VENDORS.length === 0) {
2761
+ console.error("[ornold] No antidetect vendor ports configured. Only browser_* and captcha_* tools will be available.");
2762
+ }
2763
+ for (const [name, port] of [["linken", LINKEN_PORT], ["dolphin", DOLPHIN_PORT]]) {
2764
+ if (port === void 0) continue;
2765
+ try {
2766
+ const resp = await fetch(`http://127.0.0.1:${port}/sessions`, { signal: AbortSignal.timeout(2e3) });
2767
+ if (resp.ok) console.error(`[ornold] ${name}: detected on port ${port}`);
2768
+ } catch {
2769
+ }
2770
+ }
2771
+ ws?.send(JSON.stringify({
2772
+ type: "bridge_hello",
2773
+ hostname: (await import("os")).hostname(),
2774
+ config: {
2775
+ enabledVendors: ENABLED_VENDORS,
2776
+ ...LINKEN_PORT !== void 0 ? { linkenPort: LINKEN_PORT } : {},
2777
+ ...DOLPHIN_PORT !== void 0 ? { dolphinPort: DOLPHIN_PORT } : {}
2778
+ }
2779
+ }));
2780
+ const server = createServer();
2781
+ const transport = new StdioServerTransport();
2782
+ await server.connect(transport);
2783
+ console.error("[ornold] MCP server ready (stdio)");
2784
+ }
2785
+ main().catch((e) => {
2786
+ console.error(`[ornold] Fatal: ${e.message}`);
2787
+ process.exit(1);
2788
+ });