browsirai 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1226 @@
1
+ // src/cli/run.ts
2
+ import pc from "picocolors";
3
+
4
+ // src/chrome-launcher.ts
5
+ import { execSync, spawn } from "child_process";
6
+ import { existsSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
7
+ import http from "http";
8
+ import { join } from "path";
9
+ import { homedir, tmpdir } from "os";
10
+ import { createConnection } from "net";
11
+
12
+ // src/cli/run.ts
13
+ function parseFlags(args) {
14
+ const flags = {};
15
+ let positionalIndex = 0;
16
+ for (let i = 0; i < args.length; i++) {
17
+ const arg = args[i];
18
+ if (arg.startsWith("--")) {
19
+ const eqIdx = arg.indexOf("=");
20
+ if (eqIdx !== -1) {
21
+ const key2 = arg.slice(2, eqIdx);
22
+ const value = arg.slice(eqIdx + 1);
23
+ flags[key2] = value;
24
+ } else {
25
+ const key2 = arg.slice(2);
26
+ const next = args[i + 1];
27
+ if (next && !next.startsWith("-")) {
28
+ flags[key2] = next;
29
+ i++;
30
+ } else {
31
+ flags[key2] = "true";
32
+ }
33
+ }
34
+ } else if (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg)) {
35
+ const chars = arg.slice(1);
36
+ if (chars.length === 1) {
37
+ const next = args[i + 1];
38
+ if (next && !next.startsWith("-")) {
39
+ flags[chars] = next;
40
+ i++;
41
+ } else {
42
+ flags[chars] = "true";
43
+ }
44
+ } else {
45
+ for (const ch of chars) {
46
+ flags[ch] = "true";
47
+ }
48
+ }
49
+ } else {
50
+ flags[`_${positionalIndex}`] = arg;
51
+ positionalIndex++;
52
+ }
53
+ }
54
+ return flags;
55
+ }
56
+
57
+ // src/tools/browser-click.ts
58
+ var MODIFIER_BITS = {
59
+ Alt: 1,
60
+ Control: 2,
61
+ Meta: 4,
62
+ Shift: 8
63
+ };
64
+ var REF_PATTERN = /^@e(\d+)$/;
65
+ function calculateCenter(content) {
66
+ const x = (content[0] + content[2] + content[4] + content[6]) / 4;
67
+ const y = (content[1] + content[3] + content[5] + content[7]) / 4;
68
+ return { x: Math.round(x), y: Math.round(y) };
69
+ }
70
+ async function resolveElementCoordinates(cdp, params) {
71
+ let backendNodeId;
72
+ let nodeId;
73
+ if (params.ref) {
74
+ const match = REF_PATTERN.exec(params.ref);
75
+ if (!match) {
76
+ throw new Error(`Invalid ref format: ${params.ref}`);
77
+ }
78
+ backendNodeId = parseInt(match[1], 10);
79
+ await cdp.send("DOM.resolveNode", {
80
+ backendNodeId
81
+ });
82
+ } else if (params.selector) {
83
+ const docResponse = await cdp.send("DOM.getDocument");
84
+ const queryResponse = await cdp.send("DOM.querySelector", {
85
+ nodeId: docResponse.root.nodeId,
86
+ selector: params.selector
87
+ });
88
+ if (!queryResponse.nodeId || queryResponse.nodeId === 0) {
89
+ throw new Error(
90
+ `Element not found: no element matches selector "${params.selector}"`
91
+ );
92
+ }
93
+ nodeId = queryResponse.nodeId;
94
+ } else {
95
+ throw new Error("Either ref, selector, or coordinates (x, y) must be provided");
96
+ }
97
+ const scrollParams = {};
98
+ if (backendNodeId !== void 0) {
99
+ scrollParams.backendNodeId = backendNodeId;
100
+ } else if (nodeId !== void 0) {
101
+ scrollParams.nodeId = nodeId;
102
+ }
103
+ await cdp.send(
104
+ "DOM.scrollIntoViewIfNeeded",
105
+ scrollParams
106
+ );
107
+ const boxParams = {};
108
+ if (backendNodeId !== void 0) {
109
+ boxParams.backendNodeId = backendNodeId;
110
+ } else if (nodeId !== void 0) {
111
+ boxParams.nodeId = nodeId;
112
+ }
113
+ const boxResponse = await cdp.send("DOM.getBoxModel", boxParams);
114
+ const { content, width, height } = boxResponse.model;
115
+ if (width === 0 && height === 0) {
116
+ throw new Error(
117
+ "Element is not visible: zero-size box model. The element may be hidden or not rendered."
118
+ );
119
+ }
120
+ return calculateCenter(content);
121
+ }
122
+ function computeModifiers(modifiers) {
123
+ if (!modifiers || modifiers.length === 0) return 0;
124
+ let bits = 0;
125
+ for (const mod of modifiers) {
126
+ const bit = MODIFIER_BITS[mod];
127
+ if (bit !== void 0) {
128
+ bits |= bit;
129
+ }
130
+ }
131
+ return bits;
132
+ }
133
+ function delay(ms) {
134
+ return new Promise((resolve) => setTimeout(resolve, ms));
135
+ }
136
+ async function browserClick(cdp, params) {
137
+ let x;
138
+ let y;
139
+ if (params.x !== void 0 && params.y !== void 0) {
140
+ x = params.x;
141
+ y = params.y;
142
+ } else {
143
+ const coords = await resolveElementCoordinates(cdp, params);
144
+ x = coords.x;
145
+ y = coords.y;
146
+ }
147
+ const button = params.button ?? "left";
148
+ let effectiveModifiers = params.modifiers ? [...params.modifiers] : [];
149
+ if (params.newTab) {
150
+ const isMac = typeof process !== "undefined" && process.platform === "darwin";
151
+ const newTabModifier = isMac ? "Meta" : "Control";
152
+ if (!effectiveModifiers.includes(newTabModifier)) {
153
+ effectiveModifiers.push(newTabModifier);
154
+ }
155
+ }
156
+ const modifiers = computeModifiers(effectiveModifiers);
157
+ if (params.doubleClick) {
158
+ await performDoubleClick(cdp, x, y, button, modifiers);
159
+ } else {
160
+ await performClick(cdp, x, y, button, modifiers);
161
+ }
162
+ return { success: true };
163
+ }
164
+ async function performClick(cdp, x, y, button, modifiers, clickCount = 1) {
165
+ await cdp.send("Input.dispatchMouseEvent", {
166
+ type: "mouseMoved",
167
+ x,
168
+ y,
169
+ modifiers
170
+ });
171
+ await cdp.send("Input.dispatchMouseEvent", {
172
+ type: "mousePressed",
173
+ x,
174
+ y,
175
+ button,
176
+ clickCount,
177
+ modifiers
178
+ });
179
+ await delay(50);
180
+ await cdp.send("Input.dispatchMouseEvent", {
181
+ type: "mouseReleased",
182
+ x,
183
+ y,
184
+ button,
185
+ clickCount,
186
+ modifiers
187
+ });
188
+ }
189
+ async function performDoubleClick(cdp, x, y, button, modifiers) {
190
+ await performClick(cdp, x, y, button, modifiers, 1);
191
+ await performClick(cdp, x, y, button, modifiers, 2);
192
+ }
193
+
194
+ // src/tools/browser-fill-form.ts
195
+ function parseRef(ref) {
196
+ const match = ref.match(/@?e(\d+)/);
197
+ if (!match) {
198
+ throw new Error(`Invalid ref format: ${ref}`);
199
+ }
200
+ return parseInt(match[1], 10);
201
+ }
202
+ async function resolveObjectId(cdp, field) {
203
+ if (field.ref) {
204
+ const backendNodeId = parseRef(field.ref);
205
+ const result = await cdp.send("DOM.resolveNode", {
206
+ backendNodeId
207
+ });
208
+ return result.object.objectId;
209
+ }
210
+ if (field.selector) {
211
+ const evalResult = await cdp.send("Runtime.evaluate", {
212
+ expression: `document.querySelector(${JSON.stringify(field.selector)})`,
213
+ returnByValue: false
214
+ });
215
+ if (evalResult.result.objectId) {
216
+ return evalResult.result.objectId;
217
+ }
218
+ const docResult = await cdp.send(
219
+ "DOM.getDocument",
220
+ {}
221
+ );
222
+ const queryResult = await cdp.send("DOM.querySelector", {
223
+ nodeId: docResult.root.nodeId,
224
+ selector: field.selector
225
+ });
226
+ const resolveResult = await cdp.send("DOM.resolveNode", {
227
+ nodeId: queryResult.nodeId
228
+ });
229
+ return resolveResult.object.objectId;
230
+ }
231
+ throw new Error(`Field "${field.name}" has neither ref nor selector`);
232
+ }
233
+ async function isReadonlyOrDisabled(cdp, objectId) {
234
+ const result = await cdp.send("Runtime.callFunctionOn", {
235
+ objectId,
236
+ functionDeclaration: `function() { return this.readOnly || this.disabled; }`,
237
+ returnByValue: true
238
+ });
239
+ return result.result.value === true;
240
+ }
241
+ async function clickElement(cdp, field) {
242
+ let backendNodeId;
243
+ if (field.ref) {
244
+ backendNodeId = parseRef(field.ref);
245
+ }
246
+ if (backendNodeId !== void 0) {
247
+ await cdp.send("DOM.scrollIntoViewIfNeeded", {
248
+ backendNodeId
249
+ });
250
+ }
251
+ const boxResult = await cdp.send("DOM.getBoxModel", {
252
+ backendNodeId
253
+ });
254
+ const quad = boxResult.model.content;
255
+ const centerX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
256
+ const centerY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
257
+ await cdp.send("Input.dispatchMouseEvent", {
258
+ type: "mousePressed",
259
+ x: centerX,
260
+ y: centerY,
261
+ button: "left",
262
+ clickCount: 1
263
+ });
264
+ await cdp.send("Input.dispatchMouseEvent", {
265
+ type: "mouseReleased",
266
+ x: centerX,
267
+ y: centerY,
268
+ button: "left",
269
+ clickCount: 1
270
+ });
271
+ }
272
+ async function fillTextbox(cdp, field, objectId) {
273
+ if (field.ref) {
274
+ const backendNodeId = parseRef(field.ref);
275
+ await cdp.send("DOM.focus", {
276
+ backendNodeId
277
+ });
278
+ } else {
279
+ await cdp.send("Runtime.callFunctionOn", {
280
+ objectId,
281
+ functionDeclaration: `function() { this.focus(); }`,
282
+ returnByValue: true
283
+ });
284
+ }
285
+ await cdp.send("Runtime.callFunctionOn", {
286
+ objectId,
287
+ functionDeclaration: `function() { this.value = ''; this.dispatchEvent(new Event('input', { bubbles: true })); }`,
288
+ returnByValue: true
289
+ });
290
+ await cdp.send("Input.insertText", {
291
+ text: field.value
292
+ });
293
+ await cdp.send("Runtime.callFunctionOn", {
294
+ objectId,
295
+ functionDeclaration: `function() {
296
+ this.dispatchEvent(new Event('input', { bubbles: true }));
297
+ this.dispatchEvent(new Event('change', { bubbles: true }));
298
+ }`,
299
+ returnByValue: true
300
+ });
301
+ }
302
+ async function fillCombobox(cdp, _field, objectId, value) {
303
+ await cdp.send("Runtime.callFunctionOn", {
304
+ objectId,
305
+ functionDeclaration: `function(val) {
306
+ this.value = val;
307
+ this.dispatchEvent(new Event('input', { bubbles: true }));
308
+ this.dispatchEvent(new Event('change', { bubbles: true }));
309
+ return [val];
310
+ }`,
311
+ arguments: [{ value }],
312
+ returnByValue: true
313
+ });
314
+ }
315
+ async function fillSlider(cdp, _field, objectId, value) {
316
+ await cdp.send("Runtime.callFunctionOn", {
317
+ objectId,
318
+ functionDeclaration: `function(val) {
319
+ this.value = val;
320
+ this.dispatchEvent(new Event('input', { bubbles: true }));
321
+ this.dispatchEvent(new Event('change', { bubbles: true }));
322
+ }`,
323
+ arguments: [{ value }],
324
+ returnByValue: true
325
+ });
326
+ }
327
+ async function browserFillForm(cdp, params) {
328
+ let filledCount = 0;
329
+ const errors = [];
330
+ for (const field of params.fields) {
331
+ try {
332
+ const objectId = await resolveObjectId(cdp, field);
333
+ switch (field.type) {
334
+ case "textbox": {
335
+ const isBlocked = await isReadonlyOrDisabled(cdp, objectId);
336
+ if (isBlocked) {
337
+ errors.push({
338
+ field: field.name,
339
+ error: `Cannot fill readonly or disabled field "${field.name}"`
340
+ });
341
+ continue;
342
+ }
343
+ await fillTextbox(cdp, field, objectId);
344
+ filledCount++;
345
+ break;
346
+ }
347
+ case "checkbox": {
348
+ await clickElement(cdp, field);
349
+ filledCount++;
350
+ break;
351
+ }
352
+ case "radio": {
353
+ await clickElement(cdp, field);
354
+ filledCount++;
355
+ break;
356
+ }
357
+ case "combobox": {
358
+ await fillCombobox(cdp, field, objectId, field.value);
359
+ filledCount++;
360
+ break;
361
+ }
362
+ case "slider": {
363
+ await fillSlider(cdp, field, objectId, field.value);
364
+ filledCount++;
365
+ break;
366
+ }
367
+ default: {
368
+ await fillTextbox(cdp, field, objectId);
369
+ filledCount++;
370
+ break;
371
+ }
372
+ }
373
+ } catch (err) {
374
+ errors.push({
375
+ field: field.name,
376
+ error: err instanceof Error ? err.message : `Unknown error filling ${field.name}`
377
+ });
378
+ }
379
+ }
380
+ return {
381
+ success: errors.length === 0,
382
+ filledCount,
383
+ errors: errors.length > 0 ? errors : void 0
384
+ };
385
+ }
386
+
387
+ // src/tools/browser-type.ts
388
+ function parseRef2(ref) {
389
+ const match = ref.match(/@e(\d+)/);
390
+ if (!match) {
391
+ throw new Error(`Invalid ref format: ${ref}`);
392
+ }
393
+ return parseInt(match[1], 10);
394
+ }
395
+ function getKeyCode(key2) {
396
+ if (key2.length === 1) {
397
+ return key2.toUpperCase().charCodeAt(0);
398
+ }
399
+ const codes = {
400
+ Enter: 13,
401
+ Tab: 9,
402
+ Backspace: 8
403
+ };
404
+ return codes[key2] ?? 0;
405
+ }
406
+ function getCode(key2) {
407
+ if (key2.length === 1) {
408
+ const upper = key2.toUpperCase();
409
+ if (upper >= "A" && upper <= "Z") return `Key${upper}`;
410
+ if (upper >= "0" && upper <= "9") return `Digit${upper}`;
411
+ }
412
+ return key2;
413
+ }
414
+ function delay2(ms) {
415
+ return new Promise((resolve) => setTimeout(resolve, ms));
416
+ }
417
+ async function dispatchCharEvents(cdp, char) {
418
+ const keyCode = getKeyCode(char);
419
+ const code = getCode(char);
420
+ await cdp.send("Input.dispatchKeyEvent", {
421
+ type: "keyDown",
422
+ key: char,
423
+ code,
424
+ windowsVirtualKeyCode: keyCode,
425
+ text: char
426
+ });
427
+ await cdp.send("Input.dispatchKeyEvent", {
428
+ type: "char",
429
+ key: char,
430
+ code,
431
+ windowsVirtualKeyCode: keyCode,
432
+ text: char
433
+ });
434
+ await cdp.send("Input.dispatchKeyEvent", {
435
+ type: "keyUp",
436
+ key: char,
437
+ code,
438
+ windowsVirtualKeyCode: keyCode
439
+ });
440
+ }
441
+ async function browserType(cdp, params) {
442
+ if (params.ref) {
443
+ const backendNodeId = parseRef2(params.ref);
444
+ await cdp.send("DOM.focus", {
445
+ backendNodeId
446
+ });
447
+ }
448
+ if (!params.ref && params.selector) {
449
+ const evalResult = await cdp.send("Runtime.evaluate", {
450
+ expression: `document.querySelector(${JSON.stringify(params.selector)})`,
451
+ returnByValue: false
452
+ });
453
+ if (evalResult.result.objectId) {
454
+ await cdp.send("Runtime.callFunctionOn", {
455
+ objectId: evalResult.result.objectId,
456
+ functionDeclaration: `function() { this.focus(); }`,
457
+ returnByValue: true
458
+ });
459
+ }
460
+ }
461
+ if (params.slowly) {
462
+ for (let i = 0; i < params.text.length; i++) {
463
+ await dispatchCharEvents(cdp, params.text[i]);
464
+ if (i < params.text.length - 1) {
465
+ await delay2(50);
466
+ }
467
+ }
468
+ } else {
469
+ await cdp.send("Input.insertText", {
470
+ text: params.text
471
+ });
472
+ }
473
+ if (params.submit) {
474
+ await cdp.send("Input.dispatchKeyEvent", {
475
+ type: "keyDown",
476
+ key: "Enter",
477
+ code: "Enter",
478
+ windowsVirtualKeyCode: 13
479
+ });
480
+ await cdp.send("Input.dispatchKeyEvent", {
481
+ type: "keyUp",
482
+ key: "Enter",
483
+ code: "Enter",
484
+ windowsVirtualKeyCode: 13
485
+ });
486
+ }
487
+ return { success: true };
488
+ }
489
+
490
+ // src/tools/browser-press-key.ts
491
+ var KEY_CODE_MAP = {
492
+ Enter: 13,
493
+ Tab: 9,
494
+ Escape: 27,
495
+ Backspace: 8,
496
+ Delete: 46,
497
+ ArrowLeft: 37,
498
+ ArrowUp: 38,
499
+ ArrowRight: 39,
500
+ ArrowDown: 40,
501
+ Home: 36,
502
+ End: 35,
503
+ PageUp: 33,
504
+ PageDown: 34,
505
+ Insert: 45,
506
+ Space: 32,
507
+ " ": 32,
508
+ F1: 112,
509
+ F2: 113,
510
+ F3: 114,
511
+ F4: 115,
512
+ F5: 116,
513
+ F6: 117,
514
+ F7: 118,
515
+ F8: 119,
516
+ F9: 120,
517
+ F10: 121,
518
+ F11: 122,
519
+ F12: 123,
520
+ Control: 17,
521
+ Shift: 16,
522
+ Alt: 18,
523
+ Meta: 91
524
+ };
525
+ var KEY_TO_CODE = {
526
+ Enter: "Enter",
527
+ Tab: "Tab",
528
+ Escape: "Escape",
529
+ Backspace: "Backspace",
530
+ Delete: "Delete",
531
+ ArrowLeft: "ArrowLeft",
532
+ ArrowUp: "ArrowUp",
533
+ ArrowRight: "ArrowRight",
534
+ ArrowDown: "ArrowDown",
535
+ Home: "Home",
536
+ End: "End",
537
+ PageUp: "PageUp",
538
+ PageDown: "PageDown",
539
+ Insert: "Insert",
540
+ Space: "Space",
541
+ " ": "Space",
542
+ Control: "ControlLeft",
543
+ Shift: "ShiftLeft",
544
+ Alt: "AltLeft",
545
+ Meta: "MetaLeft"
546
+ };
547
+ var KEY_TEXT_MAP = {
548
+ Enter: "\r",
549
+ Tab: " ",
550
+ Escape: "",
551
+ Space: " ",
552
+ Backspace: "\b"
553
+ };
554
+ var MODIFIER_BITS2 = {
555
+ Alt: 1,
556
+ Control: 2,
557
+ Meta: 4,
558
+ Shift: 8
559
+ };
560
+ function getKeyCode2(key2) {
561
+ if (KEY_CODE_MAP[key2] !== void 0) {
562
+ return KEY_CODE_MAP[key2];
563
+ }
564
+ if (key2.length === 1) {
565
+ return key2.toUpperCase().charCodeAt(0);
566
+ }
567
+ return 0;
568
+ }
569
+ function getCode2(key2) {
570
+ if (KEY_TO_CODE[key2] !== void 0) {
571
+ return KEY_TO_CODE[key2];
572
+ }
573
+ if (key2.length === 1) {
574
+ const upper = key2.toUpperCase();
575
+ if (upper >= "A" && upper <= "Z") {
576
+ return `Key${upper}`;
577
+ }
578
+ if (upper >= "0" && upper <= "9") {
579
+ return `Digit${upper}`;
580
+ }
581
+ }
582
+ return key2;
583
+ }
584
+ function getKeyText(key2) {
585
+ if (KEY_TEXT_MAP[key2] !== void 0) {
586
+ return KEY_TEXT_MAP[key2] || void 0;
587
+ }
588
+ if (key2.length === 1) {
589
+ return key2;
590
+ }
591
+ return void 0;
592
+ }
593
+ async function browserPressKey(cdp, params) {
594
+ const parts = params.key.split("+");
595
+ const mainKey = parts[parts.length - 1];
596
+ const modifierKeys = parts.slice(0, -1);
597
+ let modifiers = 0;
598
+ for (const mod of modifierKeys) {
599
+ if (MODIFIER_BITS2[mod] !== void 0) {
600
+ modifiers |= MODIFIER_BITS2[mod];
601
+ }
602
+ }
603
+ for (const mod of modifierKeys) {
604
+ await cdp.send("Input.dispatchKeyEvent", {
605
+ type: "keyDown",
606
+ key: mod,
607
+ code: getCode2(mod),
608
+ windowsVirtualKeyCode: getKeyCode2(mod),
609
+ modifiers
610
+ });
611
+ }
612
+ const text = getKeyText(mainKey);
613
+ const keyDownParams = {
614
+ type: "keyDown",
615
+ key: mainKey,
616
+ code: getCode2(mainKey),
617
+ windowsVirtualKeyCode: getKeyCode2(mainKey),
618
+ modifiers
619
+ };
620
+ if (text !== void 0) {
621
+ keyDownParams.text = text;
622
+ }
623
+ await cdp.send("Input.dispatchKeyEvent", keyDownParams);
624
+ if (text && modifierKeys.length === 0) {
625
+ await cdp.send("Input.dispatchKeyEvent", {
626
+ type: "char",
627
+ key: mainKey,
628
+ code: getCode2(mainKey),
629
+ windowsVirtualKeyCode: getKeyCode2(mainKey),
630
+ modifiers,
631
+ text
632
+ });
633
+ }
634
+ await cdp.send("Input.dispatchKeyEvent", {
635
+ type: "keyUp",
636
+ key: mainKey,
637
+ code: getCode2(mainKey),
638
+ windowsVirtualKeyCode: getKeyCode2(mainKey),
639
+ modifiers
640
+ });
641
+ for (let i = modifierKeys.length - 1; i >= 0; i--) {
642
+ const mod = modifierKeys[i];
643
+ await cdp.send("Input.dispatchKeyEvent", {
644
+ type: "keyUp",
645
+ key: mod,
646
+ code: getCode2(mod),
647
+ windowsVirtualKeyCode: getKeyCode2(mod),
648
+ modifiers: 0
649
+ });
650
+ }
651
+ return { success: true };
652
+ }
653
+
654
+ // src/tools/browser-hover.ts
655
+ var REF_PATTERN2 = /^@?e(\d+)$/;
656
+ function calculateCenter2(content) {
657
+ const x = (content[0] + content[2] + content[4] + content[6]) / 4;
658
+ const y = (content[1] + content[3] + content[5] + content[7]) / 4;
659
+ return { x: Math.round(x), y: Math.round(y) };
660
+ }
661
+ async function resolveElementCoordinates2(cdp, params) {
662
+ let backendNodeId;
663
+ let nodeId;
664
+ if (params.ref) {
665
+ const match = REF_PATTERN2.exec(params.ref);
666
+ if (!match) {
667
+ throw new Error(`Invalid ref format: ${params.ref}`);
668
+ }
669
+ backendNodeId = parseInt(match[1], 10);
670
+ await cdp.send("DOM.resolveNode", {
671
+ backendNodeId
672
+ });
673
+ } else if (params.selector) {
674
+ const docResponse = await cdp.send("DOM.getDocument");
675
+ const queryResponse = await cdp.send("DOM.querySelector", {
676
+ nodeId: docResponse.root.nodeId,
677
+ selector: params.selector
678
+ });
679
+ if (!queryResponse.nodeId || queryResponse.nodeId === 0) {
680
+ throw new Error(
681
+ `Element not found: no element matches selector "${params.selector}"`
682
+ );
683
+ }
684
+ nodeId = queryResponse.nodeId;
685
+ } else {
686
+ throw new Error("Either ref or selector must be provided");
687
+ }
688
+ const scrollParams = {};
689
+ if (backendNodeId !== void 0) {
690
+ scrollParams.backendNodeId = backendNodeId;
691
+ } else if (nodeId !== void 0) {
692
+ scrollParams.nodeId = nodeId;
693
+ }
694
+ await cdp.send("DOM.scrollIntoViewIfNeeded", scrollParams);
695
+ const boxParams = {};
696
+ if (backendNodeId !== void 0) {
697
+ boxParams.backendNodeId = backendNodeId;
698
+ } else if (nodeId !== void 0) {
699
+ boxParams.nodeId = nodeId;
700
+ }
701
+ const boxResponse = await cdp.send("DOM.getBoxModel", boxParams);
702
+ return calculateCenter2(boxResponse.model.content);
703
+ }
704
+ async function browserHover(cdp, params) {
705
+ const { x, y } = await resolveElementCoordinates2(cdp, params);
706
+ await cdp.send("Input.dispatchMouseEvent", {
707
+ type: "mouseMoved",
708
+ x,
709
+ y
710
+ });
711
+ return { success: true };
712
+ }
713
+
714
+ // src/tools/browser-drag.ts
715
+ var REF_PATTERN3 = /^@?e(\d+)$/;
716
+ function calculateCenter3(content) {
717
+ const x = (content[0] + content[2] + content[4] + content[6]) / 4;
718
+ const y = (content[1] + content[3] + content[5] + content[7]) / 4;
719
+ return { x: Math.round(x), y: Math.round(y) };
720
+ }
721
+ async function resolveRefCoordinates(cdp, ref) {
722
+ const match = REF_PATTERN3.exec(ref);
723
+ if (!match) {
724
+ throw new Error(`Invalid ref format: ${ref}`);
725
+ }
726
+ const backendNodeId = parseInt(match[1], 10);
727
+ const opts = { timeout: 5e3 };
728
+ await cdp.send("DOM.resolveNode", {
729
+ backendNodeId
730
+ }, opts);
731
+ try {
732
+ await cdp.send("DOM.scrollIntoViewIfNeeded", {
733
+ backendNodeId
734
+ }, opts);
735
+ } catch {
736
+ }
737
+ const boxResponse = await cdp.send("DOM.getBoxModel", {
738
+ backendNodeId
739
+ }, opts);
740
+ return calculateCenter3(boxResponse.model.content);
741
+ }
742
+ function delay3(ms) {
743
+ return new Promise((resolve) => setTimeout(resolve, ms));
744
+ }
745
+ function interpolatePoints(startX, startY, endX, endY, steps = 8) {
746
+ const points = [];
747
+ for (let i = 1; i < steps; i++) {
748
+ const t = i / steps;
749
+ points.push({
750
+ x: Math.round(startX + (endX - startX) * t),
751
+ y: Math.round(startY + (endY - startY) * t)
752
+ });
753
+ }
754
+ return points;
755
+ }
756
+ async function browserDrag(cdp, params) {
757
+ let startX;
758
+ let startY;
759
+ if (params.startX !== void 0 && params.startY !== void 0) {
760
+ startX = params.startX;
761
+ startY = params.startY;
762
+ } else if (params.startRef) {
763
+ const coords = await resolveRefCoordinates(cdp, params.startRef);
764
+ startX = coords.x;
765
+ startY = coords.y;
766
+ } else {
767
+ throw new Error("Either startRef or startX/startY must be provided");
768
+ }
769
+ let endX;
770
+ let endY;
771
+ if (params.endX !== void 0 && params.endY !== void 0) {
772
+ endX = params.endX;
773
+ endY = params.endY;
774
+ } else if (params.endRef) {
775
+ const coords = await resolveRefCoordinates(cdp, params.endRef);
776
+ endX = coords.x;
777
+ endY = coords.y;
778
+ } else {
779
+ throw new Error("Either endRef or endX/endY must be provided");
780
+ }
781
+ const mouseOpts = { timeout: 3e3 };
782
+ await cdp.send("Input.dispatchMouseEvent", {
783
+ type: "mouseMoved",
784
+ x: startX,
785
+ y: startY
786
+ }, mouseOpts);
787
+ await cdp.send("Input.dispatchMouseEvent", {
788
+ type: "mousePressed",
789
+ x: startX,
790
+ y: startY,
791
+ button: "left",
792
+ clickCount: 1
793
+ }, mouseOpts);
794
+ const intermediatePoints = interpolatePoints(startX, startY, endX, endY, 4);
795
+ for (const point of intermediatePoints) {
796
+ await cdp.send("Input.dispatchMouseEvent", {
797
+ type: "mouseMoved",
798
+ x: point.x,
799
+ y: point.y
800
+ }, mouseOpts);
801
+ await delay3(10);
802
+ }
803
+ await cdp.send("Input.dispatchMouseEvent", {
804
+ type: "mouseMoved",
805
+ x: endX,
806
+ y: endY
807
+ }, mouseOpts);
808
+ await cdp.send("Input.dispatchMouseEvent", {
809
+ type: "mouseReleased",
810
+ x: endX,
811
+ y: endY,
812
+ button: "left",
813
+ clickCount: 1
814
+ }, mouseOpts);
815
+ return { success: true };
816
+ }
817
+
818
+ // src/tools/browser-select-option.ts
819
+ function refToBackendNodeId(ref) {
820
+ const match = /^@?e(\d+)$/.exec(ref);
821
+ if (!match) {
822
+ throw new Error(`Invalid ref format: ${ref}`);
823
+ }
824
+ return parseInt(match[1], 10);
825
+ }
826
+ async function browserSelectOption(cdp, params) {
827
+ const backendNodeId = refToBackendNodeId(params.ref);
828
+ const resolveResponse = await cdp.send("DOM.resolveNode", {
829
+ backendNodeId
830
+ });
831
+ const objectId = resolveResponse.object.objectId;
832
+ const valuesJson = JSON.stringify(params.values);
833
+ const result = await cdp.send("Runtime.callFunctionOn", {
834
+ objectId,
835
+ functionDeclaration: `function() {
836
+ var select = this;
837
+ if (select.tagName !== 'SELECT') {
838
+ throw new Error('Element is not a SELECT');
839
+ }
840
+ var values = ${valuesJson};
841
+ var matched = [];
842
+
843
+ for (var i = 0; i < select.options.length; i++) {
844
+ var option = select.options[i];
845
+ var isMatch = values.indexOf(option.value) !== -1 ||
846
+ values.indexOf(option.textContent.trim()) !== -1;
847
+ option.selected = isMatch;
848
+ if (isMatch) {
849
+ matched.push(option.value);
850
+ }
851
+ }
852
+
853
+ select.dispatchEvent(new Event('input', { bubbles: true }));
854
+ select.dispatchEvent(new Event('change', { bubbles: true }));
855
+
856
+ return matched;
857
+ }`,
858
+ returnByValue: true
859
+ });
860
+ const selected = result.result.value ?? [];
861
+ return {
862
+ success: true,
863
+ selected
864
+ };
865
+ }
866
+
867
+ // src/tools/browser-file-upload.ts
868
+ var REF_PATTERN4 = /^@e(\d+)$/;
869
+ async function browserFileUpload(cdp, params) {
870
+ const match = REF_PATTERN4.exec(params.ref);
871
+ if (!match) {
872
+ return {
873
+ success: false,
874
+ filesCount: 0,
875
+ error: `Invalid ref format: ${params.ref}. Expected @eN pattern (e.g. @e5).`
876
+ };
877
+ }
878
+ const backendNodeId = parseInt(match[1], 10);
879
+ const resolved = await cdp.send("DOM.resolveNode", {
880
+ backendNodeId
881
+ });
882
+ const objectId = resolved.object.objectId;
883
+ await cdp.send("DOM.setFileInputFiles", {
884
+ files: params.paths,
885
+ objectId
886
+ });
887
+ return {
888
+ success: true,
889
+ filesCount: params.paths.length
890
+ };
891
+ }
892
+
893
+ // src/tools/browser-handle-dialog.ts
894
+ async function browserHandleDialog(cdp, params) {
895
+ const dialogParams = {
896
+ accept: params.accept
897
+ };
898
+ if (params.promptText !== void 0) {
899
+ dialogParams.promptText = params.promptText;
900
+ }
901
+ try {
902
+ await cdp.send("Page.handleJavaScriptDialog", dialogParams);
903
+ return { success: true };
904
+ } catch (err) {
905
+ const message = err instanceof Error ? err.message : String(err);
906
+ if (message.includes("No dialog is showing") || message.includes("no dialog")) {
907
+ return {
908
+ success: false,
909
+ error: "No JavaScript dialog is currently pending. A dialog must be open before it can be handled."
910
+ };
911
+ }
912
+ return {
913
+ success: false,
914
+ error: `Failed to handle dialog: ${message}`
915
+ };
916
+ }
917
+ }
918
+
919
+ // src/cli/commands/act.ts
920
+ function normaliseRef(input) {
921
+ const cleaned = input.startsWith("@") ? input.slice(1) : input;
922
+ const match = /^e(\d+)$/i.exec(cleaned);
923
+ if (!match) {
924
+ throw new Error(`Invalid ref format: ${input}. Expected @eN (e.g. @e5).`);
925
+ }
926
+ return `@e${match[1]}`;
927
+ }
928
+ function looksLikeRef(arg) {
929
+ return /^@?e\d+$/i.test(arg);
930
+ }
931
+ var click = {
932
+ name: "click",
933
+ description: "Click an element by ref or CSS selector",
934
+ usage: "browsirai click <ref-or-selector> [--newTab]",
935
+ async run(cdp, args) {
936
+ const flags = parseFlags(args);
937
+ const target = flags._0;
938
+ if (!target) {
939
+ console.error("Usage: browsirai click <ref-or-selector> [--newTab]");
940
+ console.error(" Provide an @eN ref or CSS selector as the first argument.");
941
+ process.exit(1);
942
+ }
943
+ const newTab = flags.newTab === "true";
944
+ try {
945
+ if (looksLikeRef(target)) {
946
+ const ref = normaliseRef(target);
947
+ await browserClick(cdp, { ref, newTab });
948
+ console.log(`Clicked ${ref}`);
949
+ } else {
950
+ await browserClick(cdp, { selector: target, newTab });
951
+ console.log(`Clicked ${target}`);
952
+ }
953
+ } catch (err) {
954
+ const msg = err instanceof Error ? err.message : String(err);
955
+ console.error(`Click failed: ${msg}`);
956
+ process.exit(1);
957
+ }
958
+ }
959
+ };
960
+ var fill = {
961
+ name: "fill",
962
+ description: "Fill a form field with a value",
963
+ usage: "browsirai fill <ref-or-selector> <value>",
964
+ async run(cdp, args) {
965
+ const flags = parseFlags(args);
966
+ const target = flags._0;
967
+ const value = flags._1;
968
+ if (!target || value === void 0) {
969
+ console.error("Usage: browsirai fill <ref-or-selector> <value>");
970
+ console.error(" Provide an @eN ref or CSS selector and a value.");
971
+ process.exit(1);
972
+ }
973
+ try {
974
+ const isRef = looksLikeRef(target);
975
+ const ref = isRef ? normaliseRef(target) : void 0;
976
+ const selector = isRef ? void 0 : target;
977
+ const label = isRef ? normaliseRef(target) : target;
978
+ await browserFillForm(cdp, {
979
+ fields: [
980
+ {
981
+ name: label,
982
+ type: "textbox",
983
+ ref,
984
+ selector,
985
+ value
986
+ }
987
+ ]
988
+ });
989
+ const displayValue = value.length > 40 ? value.slice(0, 40) + "..." : value;
990
+ console.log(`Filled ${label} with '${displayValue}'`);
991
+ } catch (err) {
992
+ const msg = err instanceof Error ? err.message : String(err);
993
+ console.error(`Fill failed: ${msg}`);
994
+ process.exit(1);
995
+ }
996
+ }
997
+ };
998
+ var type = {
999
+ name: "type",
1000
+ description: "Type text into the focused or specified element",
1001
+ usage: "browsirai type <text> [--ref=@e3] [--submit] [--slowly]",
1002
+ async run(cdp, args) {
1003
+ const flags = parseFlags(args);
1004
+ const text = flags._0;
1005
+ if (!text) {
1006
+ console.error("Usage: browsirai type <text> [--ref=@e3] [--submit] [--slowly]");
1007
+ console.error(" Provide the text to type as the first argument.");
1008
+ process.exit(1);
1009
+ }
1010
+ const ref = flags.ref ? normaliseRef(flags.ref) : void 0;
1011
+ const selector = flags.selector;
1012
+ const submit = flags.submit === "true";
1013
+ const slowly = flags.slowly === "true";
1014
+ try {
1015
+ await browserType(cdp, { text, ref, selector, slowly, submit });
1016
+ const displayText = text.length > 40 ? text.slice(0, 40) + "..." : text;
1017
+ const target = ref ?? selector ?? "focused element";
1018
+ console.log(`Typed '${displayText}' into ${target}`);
1019
+ } catch (err) {
1020
+ const msg = err instanceof Error ? err.message : String(err);
1021
+ console.error(`Type failed: ${msg}`);
1022
+ process.exit(1);
1023
+ }
1024
+ }
1025
+ };
1026
+ var key = {
1027
+ name: "press",
1028
+ aliases: ["key"],
1029
+ description: "Press a key or key combination",
1030
+ usage: "browsirai press <key-combo>",
1031
+ async run(cdp, args) {
1032
+ const flags = parseFlags(args);
1033
+ const keyCombo = flags._0;
1034
+ if (!keyCombo) {
1035
+ console.error("Usage: browsirai key <key-combo>");
1036
+ console.error(" Examples: Enter, Tab, Control+c, Shift+Tab, Meta+a");
1037
+ process.exit(1);
1038
+ }
1039
+ try {
1040
+ await browserPressKey(cdp, { key: keyCombo });
1041
+ console.log(`Pressed ${keyCombo}`);
1042
+ } catch (err) {
1043
+ const msg = err instanceof Error ? err.message : String(err);
1044
+ console.error(`Key press failed: ${msg}`);
1045
+ process.exit(1);
1046
+ }
1047
+ }
1048
+ };
1049
+ var hover = {
1050
+ name: "hover",
1051
+ description: "Hover over an element by ref or selector",
1052
+ usage: "browsirai hover <ref-or-selector>",
1053
+ async run(cdp, args) {
1054
+ const flags = parseFlags(args);
1055
+ const target = flags._0;
1056
+ if (!target) {
1057
+ console.error("Usage: browsirai hover <ref-or-selector>");
1058
+ console.error(" Provide an @eN ref or CSS selector.");
1059
+ process.exit(1);
1060
+ }
1061
+ try {
1062
+ if (looksLikeRef(target)) {
1063
+ const ref = normaliseRef(target);
1064
+ await browserHover(cdp, { ref });
1065
+ console.log(`Hovered ${ref}`);
1066
+ } else {
1067
+ await browserHover(cdp, { selector: target });
1068
+ console.log(`Hovered ${target}`);
1069
+ }
1070
+ } catch (err) {
1071
+ const msg = err instanceof Error ? err.message : String(err);
1072
+ console.error(`Hover failed: ${msg}`);
1073
+ process.exit(1);
1074
+ }
1075
+ }
1076
+ };
1077
+ var drag = {
1078
+ name: "drag",
1079
+ description: "Drag from one element to another",
1080
+ usage: "browsirai drag <startRef> <endRef>",
1081
+ async run(cdp, args) {
1082
+ const flags = parseFlags(args);
1083
+ const startArg = flags._0;
1084
+ const endArg = flags._1;
1085
+ if (!startArg || !endArg) {
1086
+ console.error("Usage: browsirai drag <startRef> <endRef>");
1087
+ console.error(" Provide two @eN refs (source and target).");
1088
+ process.exit(1);
1089
+ }
1090
+ try {
1091
+ const startRef = normaliseRef(startArg);
1092
+ const endRef = normaliseRef(endArg);
1093
+ await browserDrag(cdp, { startRef, endRef });
1094
+ console.log(`Dragged ${startRef} \u2192 ${endRef}`);
1095
+ } catch (err) {
1096
+ const msg = err instanceof Error ? err.message : String(err);
1097
+ console.error(`Drag failed: ${msg}`);
1098
+ process.exit(1);
1099
+ }
1100
+ }
1101
+ };
1102
+ var select = {
1103
+ name: "select",
1104
+ description: "Select option(s) in a <select> element",
1105
+ usage: "browsirai select <ref> <value1> [value2...]",
1106
+ async run(cdp, args) {
1107
+ const flags = parseFlags(args);
1108
+ const refArg = flags._0;
1109
+ if (!refArg) {
1110
+ console.error("Usage: browsirai select <ref> <value1> [value2...]");
1111
+ console.error(" Provide an @eN ref and one or more values to select.");
1112
+ process.exit(1);
1113
+ }
1114
+ const values = [];
1115
+ let i = 1;
1116
+ while (flags[`_${i}`] !== void 0) {
1117
+ values.push(flags[`_${i}`]);
1118
+ i++;
1119
+ }
1120
+ if (values.length === 0) {
1121
+ console.error("Usage: browsirai select <ref> <value1> [value2...]");
1122
+ console.error(" Provide at least one value to select.");
1123
+ process.exit(1);
1124
+ }
1125
+ try {
1126
+ const ref = normaliseRef(refArg);
1127
+ const result = await browserSelectOption(cdp, {
1128
+ ref,
1129
+ values,
1130
+ element: ref
1131
+ });
1132
+ const displayValues = result.selected.length > 0 ? result.selected.map((v) => `'${v}'`).join(", ") : values.map((v) => `'${v}'`).join(", ");
1133
+ console.log(`Selected ${displayValues} in ${ref}`);
1134
+ } catch (err) {
1135
+ const msg = err instanceof Error ? err.message : String(err);
1136
+ console.error(`Select failed: ${msg}`);
1137
+ process.exit(1);
1138
+ }
1139
+ }
1140
+ };
1141
+ var upload = {
1142
+ name: "upload",
1143
+ description: "Upload file(s) to a file input element",
1144
+ usage: "browsirai upload <ref> <file1> [file2...]",
1145
+ async run(cdp, args) {
1146
+ const flags = parseFlags(args);
1147
+ const refArg = flags._0;
1148
+ if (!refArg) {
1149
+ console.error("Usage: browsirai upload <ref> <file1> [file2...]");
1150
+ console.error(" Provide an @eN ref and one or more file paths.");
1151
+ process.exit(1);
1152
+ }
1153
+ const paths = [];
1154
+ let i = 1;
1155
+ while (flags[`_${i}`] !== void 0) {
1156
+ paths.push(flags[`_${i}`]);
1157
+ i++;
1158
+ }
1159
+ if (paths.length === 0) {
1160
+ console.error("Usage: browsirai upload <ref> <file1> [file2...]");
1161
+ console.error(" Provide at least one file path to upload.");
1162
+ process.exit(1);
1163
+ }
1164
+ try {
1165
+ const ref = normaliseRef(refArg);
1166
+ const result = await browserFileUpload(cdp, { ref, paths });
1167
+ if (result.success) {
1168
+ console.log(`Uploaded ${result.filesCount} file(s) to ${ref}`);
1169
+ } else {
1170
+ console.error(`Upload failed: ${result.error}`);
1171
+ process.exit(1);
1172
+ }
1173
+ } catch (err) {
1174
+ const msg = err instanceof Error ? err.message : String(err);
1175
+ console.error(`Upload failed: ${msg}`);
1176
+ process.exit(1);
1177
+ }
1178
+ }
1179
+ };
1180
+ var dialog = {
1181
+ name: "dialog",
1182
+ description: "Accept or dismiss a JavaScript dialog",
1183
+ usage: "browsirai dialog <accept|dismiss> [--text=...]",
1184
+ async run(cdp, args) {
1185
+ const flags = parseFlags(args);
1186
+ const action = flags._0;
1187
+ if (!action || action !== "accept" && action !== "dismiss") {
1188
+ console.error("Usage: browsirai dialog <accept|dismiss> [--text=...]");
1189
+ console.error(" First argument must be 'accept' or 'dismiss'.");
1190
+ process.exit(1);
1191
+ }
1192
+ const accept = action === "accept";
1193
+ const promptText = flags.text;
1194
+ try {
1195
+ const result = await browserHandleDialog(cdp, {
1196
+ accept,
1197
+ promptText
1198
+ });
1199
+ if (result.success) {
1200
+ console.log(`Dialog ${accept ? "accepted" : "dismissed"}`);
1201
+ } else {
1202
+ console.error(`Dialog handling failed: ${result.error}`);
1203
+ process.exit(1);
1204
+ }
1205
+ } catch (err) {
1206
+ const msg = err instanceof Error ? err.message : String(err);
1207
+ console.error(`Dialog failed: ${msg}`);
1208
+ process.exit(1);
1209
+ }
1210
+ }
1211
+ };
1212
+ var actCommands = [
1213
+ click,
1214
+ fill,
1215
+ type,
1216
+ key,
1217
+ hover,
1218
+ drag,
1219
+ select,
1220
+ upload,
1221
+ dialog
1222
+ ];
1223
+ export {
1224
+ actCommands
1225
+ };
1226
+ //# sourceMappingURL=act.js.map