agent-browser 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.prettierrc +7 -0
  2. package/README.md +271 -1
  3. package/bin/agent-browser +2 -0
  4. package/dist/actions.d.ts +7 -0
  5. package/dist/actions.d.ts.map +1 -0
  6. package/dist/actions.js +1138 -0
  7. package/dist/actions.js.map +1 -0
  8. package/dist/browser.d.ts +232 -0
  9. package/dist/browser.d.ts.map +1 -0
  10. package/dist/browser.js +477 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/browser.test.d.ts +2 -0
  13. package/dist/browser.test.d.ts.map +1 -0
  14. package/dist/browser.test.js +136 -0
  15. package/dist/browser.test.js.map +1 -0
  16. package/dist/client.d.ts +17 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +133 -0
  19. package/dist/client.js.map +1 -0
  20. package/dist/daemon.d.ts +29 -0
  21. package/dist/daemon.d.ts.map +1 -0
  22. package/dist/daemon.js +165 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +972 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/protocol.d.ts +26 -0
  29. package/dist/protocol.d.ts.map +1 -0
  30. package/dist/protocol.js +717 -0
  31. package/dist/protocol.js.map +1 -0
  32. package/dist/protocol.test.d.ts +2 -0
  33. package/dist/protocol.test.d.ts.map +1 -0
  34. package/dist/protocol.test.js +176 -0
  35. package/dist/protocol.test.js.map +1 -0
  36. package/dist/types.d.ts +604 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +2 -0
  39. package/dist/types.js.map +1 -0
  40. package/package.json +36 -7
  41. package/src/actions.ts +1658 -0
  42. package/src/browser.test.ts +157 -0
  43. package/src/browser.ts +586 -0
  44. package/src/client.ts +150 -0
  45. package/src/daemon.ts +187 -0
  46. package/src/index.ts +984 -0
  47. package/src/protocol.test.ts +216 -0
  48. package/src/protocol.ts +848 -0
  49. package/src/types.ts +913 -0
  50. package/tsconfig.json +19 -0
  51. package/vitest.config.ts +9 -0
  52. package/index.js +0 -2
package/src/browser.ts ADDED
@@ -0,0 +1,586 @@
1
+ import {
2
+ chromium,
3
+ firefox,
4
+ webkit,
5
+ devices,
6
+ type Browser,
7
+ type BrowserContext,
8
+ type Page,
9
+ type Frame,
10
+ type Dialog,
11
+ type Request,
12
+ type Route,
13
+ } from 'playwright';
14
+ import type { LaunchCommand } from './types.js';
15
+
16
+ interface TrackedRequest {
17
+ url: string;
18
+ method: string;
19
+ headers: Record<string, string>;
20
+ timestamp: number;
21
+ resourceType: string;
22
+ }
23
+
24
+ interface ConsoleMessage {
25
+ type: string;
26
+ text: string;
27
+ timestamp: number;
28
+ }
29
+
30
+ interface PageError {
31
+ message: string;
32
+ timestamp: number;
33
+ }
34
+
35
+ /**
36
+ * Manages the Playwright browser lifecycle with multiple tabs/windows
37
+ */
38
+ export class BrowserManager {
39
+ private browser: Browser | null = null;
40
+ private contexts: BrowserContext[] = [];
41
+ private pages: Page[] = [];
42
+ private activePageIndex: number = 0;
43
+ private activeFrame: Frame | null = null;
44
+ private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
45
+ private trackedRequests: TrackedRequest[] = [];
46
+ private routes: Map<string, (route: Route) => Promise<void>> = new Map();
47
+ private consoleMessages: ConsoleMessage[] = [];
48
+ private pageErrors: PageError[] = [];
49
+ private isRecordingHar: boolean = false;
50
+
51
+ /**
52
+ * Check if browser is launched
53
+ */
54
+ isLaunched(): boolean {
55
+ return this.browser !== null;
56
+ }
57
+
58
+ /**
59
+ * Get the current active page, throws if not launched
60
+ */
61
+ getPage(): Page {
62
+ if (this.pages.length === 0) {
63
+ throw new Error('Browser not launched. Call launch first.');
64
+ }
65
+ return this.pages[this.activePageIndex];
66
+ }
67
+
68
+ /**
69
+ * Get the current frame (or page's main frame if no frame is selected)
70
+ */
71
+ getFrame(): Frame {
72
+ if (this.activeFrame) {
73
+ return this.activeFrame;
74
+ }
75
+ return this.getPage().mainFrame();
76
+ }
77
+
78
+ /**
79
+ * Switch to a frame by selector, name, or URL
80
+ */
81
+ async switchToFrame(options: { selector?: string; name?: string; url?: string }): Promise<void> {
82
+ const page = this.getPage();
83
+
84
+ if (options.selector) {
85
+ const frameElement = await page.$(options.selector);
86
+ if (!frameElement) {
87
+ throw new Error(`Frame not found: ${options.selector}`);
88
+ }
89
+ const frame = await frameElement.contentFrame();
90
+ if (!frame) {
91
+ throw new Error(`Element is not a frame: ${options.selector}`);
92
+ }
93
+ this.activeFrame = frame;
94
+ } else if (options.name) {
95
+ const frame = page.frame({ name: options.name });
96
+ if (!frame) {
97
+ throw new Error(`Frame not found with name: ${options.name}`);
98
+ }
99
+ this.activeFrame = frame;
100
+ } else if (options.url) {
101
+ const frame = page.frame({ url: options.url });
102
+ if (!frame) {
103
+ throw new Error(`Frame not found with URL: ${options.url}`);
104
+ }
105
+ this.activeFrame = frame;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Switch back to main frame
111
+ */
112
+ switchToMainFrame(): void {
113
+ this.activeFrame = null;
114
+ }
115
+
116
+ /**
117
+ * Set up dialog handler
118
+ */
119
+ setDialogHandler(response: 'accept' | 'dismiss', promptText?: string): void {
120
+ const page = this.getPage();
121
+
122
+ // Remove existing handler if any
123
+ if (this.dialogHandler) {
124
+ page.removeListener('dialog', this.dialogHandler);
125
+ }
126
+
127
+ this.dialogHandler = async (dialog: Dialog) => {
128
+ if (response === 'accept') {
129
+ await dialog.accept(promptText);
130
+ } else {
131
+ await dialog.dismiss();
132
+ }
133
+ };
134
+
135
+ page.on('dialog', this.dialogHandler);
136
+ }
137
+
138
+ /**
139
+ * Clear dialog handler
140
+ */
141
+ clearDialogHandler(): void {
142
+ if (this.dialogHandler) {
143
+ const page = this.getPage();
144
+ page.removeListener('dialog', this.dialogHandler);
145
+ this.dialogHandler = null;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Start tracking requests
151
+ */
152
+ startRequestTracking(): void {
153
+ const page = this.getPage();
154
+ page.on('request', (request: Request) => {
155
+ this.trackedRequests.push({
156
+ url: request.url(),
157
+ method: request.method(),
158
+ headers: request.headers(),
159
+ timestamp: Date.now(),
160
+ resourceType: request.resourceType(),
161
+ });
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Get tracked requests
167
+ */
168
+ getRequests(filter?: string): TrackedRequest[] {
169
+ if (filter) {
170
+ return this.trackedRequests.filter((r) => r.url.includes(filter));
171
+ }
172
+ return this.trackedRequests;
173
+ }
174
+
175
+ /**
176
+ * Clear tracked requests
177
+ */
178
+ clearRequests(): void {
179
+ this.trackedRequests = [];
180
+ }
181
+
182
+ /**
183
+ * Add a route to intercept requests
184
+ */
185
+ async addRoute(
186
+ url: string,
187
+ options: {
188
+ response?: {
189
+ status?: number;
190
+ body?: string;
191
+ contentType?: string;
192
+ headers?: Record<string, string>;
193
+ };
194
+ abort?: boolean;
195
+ }
196
+ ): Promise<void> {
197
+ const page = this.getPage();
198
+
199
+ const handler = async (route: Route) => {
200
+ if (options.abort) {
201
+ await route.abort();
202
+ } else if (options.response) {
203
+ await route.fulfill({
204
+ status: options.response.status ?? 200,
205
+ body: options.response.body ?? '',
206
+ contentType: options.response.contentType ?? 'text/plain',
207
+ headers: options.response.headers,
208
+ });
209
+ } else {
210
+ await route.continue();
211
+ }
212
+ };
213
+
214
+ this.routes.set(url, handler);
215
+ await page.route(url, handler);
216
+ }
217
+
218
+ /**
219
+ * Remove a route
220
+ */
221
+ async removeRoute(url?: string): Promise<void> {
222
+ const page = this.getPage();
223
+
224
+ if (url) {
225
+ const handler = this.routes.get(url);
226
+ if (handler) {
227
+ await page.unroute(url, handler);
228
+ this.routes.delete(url);
229
+ }
230
+ } else {
231
+ // Remove all routes
232
+ for (const [routeUrl, handler] of this.routes) {
233
+ await page.unroute(routeUrl, handler);
234
+ }
235
+ this.routes.clear();
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Set geolocation
241
+ */
242
+ async setGeolocation(latitude: number, longitude: number, accuracy?: number): Promise<void> {
243
+ const context = this.contexts[0];
244
+ if (context) {
245
+ await context.setGeolocation({ latitude, longitude, accuracy });
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Set permissions
251
+ */
252
+ async setPermissions(permissions: string[], grant: boolean): Promise<void> {
253
+ const context = this.contexts[0];
254
+ if (context) {
255
+ if (grant) {
256
+ await context.grantPermissions(permissions);
257
+ } else {
258
+ await context.clearPermissions();
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Set viewport
265
+ */
266
+ async setViewport(width: number, height: number): Promise<void> {
267
+ const page = this.getPage();
268
+ await page.setViewportSize({ width, height });
269
+ }
270
+
271
+ /**
272
+ * Get device descriptor
273
+ */
274
+ getDevice(deviceName: string): (typeof devices)[keyof typeof devices] | undefined {
275
+ return devices[deviceName as keyof typeof devices];
276
+ }
277
+
278
+ /**
279
+ * List available devices
280
+ */
281
+ listDevices(): string[] {
282
+ return Object.keys(devices);
283
+ }
284
+
285
+ /**
286
+ * Start console message tracking
287
+ */
288
+ startConsoleTracking(): void {
289
+ const page = this.getPage();
290
+ page.on('console', (msg) => {
291
+ this.consoleMessages.push({
292
+ type: msg.type(),
293
+ text: msg.text(),
294
+ timestamp: Date.now(),
295
+ });
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Get console messages
301
+ */
302
+ getConsoleMessages(): ConsoleMessage[] {
303
+ return this.consoleMessages;
304
+ }
305
+
306
+ /**
307
+ * Clear console messages
308
+ */
309
+ clearConsoleMessages(): void {
310
+ this.consoleMessages = [];
311
+ }
312
+
313
+ /**
314
+ * Start error tracking
315
+ */
316
+ startErrorTracking(): void {
317
+ const page = this.getPage();
318
+ page.on('pageerror', (error) => {
319
+ this.pageErrors.push({
320
+ message: error.message,
321
+ timestamp: Date.now(),
322
+ });
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Get page errors
328
+ */
329
+ getPageErrors(): PageError[] {
330
+ return this.pageErrors;
331
+ }
332
+
333
+ /**
334
+ * Clear page errors
335
+ */
336
+ clearPageErrors(): void {
337
+ this.pageErrors = [];
338
+ }
339
+
340
+ /**
341
+ * Start HAR recording
342
+ */
343
+ async startHarRecording(): Promise<void> {
344
+ // HAR is started at context level, flag for tracking
345
+ this.isRecordingHar = true;
346
+ }
347
+
348
+ /**
349
+ * Check if HAR recording
350
+ */
351
+ isHarRecording(): boolean {
352
+ return this.isRecordingHar;
353
+ }
354
+
355
+ /**
356
+ * Set offline mode
357
+ */
358
+ async setOffline(offline: boolean): Promise<void> {
359
+ const context = this.contexts[0];
360
+ if (context) {
361
+ await context.setOffline(offline);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Set extra HTTP headers
367
+ */
368
+ async setExtraHeaders(headers: Record<string, string>): Promise<void> {
369
+ const context = this.contexts[0];
370
+ if (context) {
371
+ await context.setExtraHTTPHeaders(headers);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Start tracing
377
+ */
378
+ async startTracing(options: { screenshots?: boolean; snapshots?: boolean }): Promise<void> {
379
+ const context = this.contexts[0];
380
+ if (context) {
381
+ await context.tracing.start({
382
+ screenshots: options.screenshots ?? true,
383
+ snapshots: options.snapshots ?? true,
384
+ });
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Stop tracing and save
390
+ */
391
+ async stopTracing(path: string): Promise<void> {
392
+ const context = this.contexts[0];
393
+ if (context) {
394
+ await context.tracing.stop({ path });
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Save storage state (cookies, localStorage, etc.)
400
+ */
401
+ async saveStorageState(path: string): Promise<void> {
402
+ const context = this.contexts[0];
403
+ if (context) {
404
+ await context.storageState({ path });
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Get all pages
410
+ */
411
+ getPages(): Page[] {
412
+ return this.pages;
413
+ }
414
+
415
+ /**
416
+ * Get current page index
417
+ */
418
+ getActiveIndex(): number {
419
+ return this.activePageIndex;
420
+ }
421
+
422
+ /**
423
+ * Get the current browser instance
424
+ */
425
+ getBrowser(): Browser | null {
426
+ return this.browser;
427
+ }
428
+
429
+ /**
430
+ * Launch the browser with the specified options
431
+ */
432
+ async launch(options: LaunchCommand): Promise<void> {
433
+ // Close existing browser if any
434
+ if (this.browser) {
435
+ await this.close();
436
+ }
437
+
438
+ // Select browser type
439
+ const browserType = options.browser ?? 'chromium';
440
+ const launcher =
441
+ browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
442
+
443
+ // Launch browser
444
+ this.browser = await launcher.launch({
445
+ headless: options.headless ?? true,
446
+ });
447
+
448
+ // Create context with viewport
449
+ const context = await this.browser.newContext({
450
+ viewport: options.viewport ?? { width: 1280, height: 720 },
451
+ });
452
+
453
+ // Set default timeout to 10 seconds (Playwright default is 30s)
454
+ context.setDefaultTimeout(10000);
455
+
456
+ this.contexts.push(context);
457
+
458
+ // Create initial page
459
+ const page = await context.newPage();
460
+ this.pages.push(page);
461
+ this.activePageIndex = 0;
462
+ }
463
+
464
+ /**
465
+ * Create a new tab in the current context
466
+ */
467
+ async newTab(): Promise<{ index: number; total: number }> {
468
+ if (!this.browser || this.contexts.length === 0) {
469
+ throw new Error('Browser not launched');
470
+ }
471
+
472
+ const context = this.contexts[0]; // Use first context for tabs
473
+ const page = await context.newPage();
474
+ this.pages.push(page);
475
+ this.activePageIndex = this.pages.length - 1;
476
+
477
+ return { index: this.activePageIndex, total: this.pages.length };
478
+ }
479
+
480
+ /**
481
+ * Create a new window (new context)
482
+ */
483
+ async newWindow(viewport?: {
484
+ width: number;
485
+ height: number;
486
+ }): Promise<{ index: number; total: number }> {
487
+ if (!this.browser) {
488
+ throw new Error('Browser not launched');
489
+ }
490
+
491
+ const context = await this.browser.newContext({
492
+ viewport: viewport ?? { width: 1280, height: 720 },
493
+ });
494
+ context.setDefaultTimeout(10000);
495
+ this.contexts.push(context);
496
+
497
+ const page = await context.newPage();
498
+ this.pages.push(page);
499
+ this.activePageIndex = this.pages.length - 1;
500
+
501
+ return { index: this.activePageIndex, total: this.pages.length };
502
+ }
503
+
504
+ /**
505
+ * Switch to a specific tab/page by index
506
+ */
507
+ switchTo(index: number): { index: number; url: string; title: string } {
508
+ if (index < 0 || index >= this.pages.length) {
509
+ throw new Error(`Invalid tab index: ${index}. Available: 0-${this.pages.length - 1}`);
510
+ }
511
+
512
+ this.activePageIndex = index;
513
+ const page = this.pages[index];
514
+
515
+ return {
516
+ index: this.activePageIndex,
517
+ url: page.url(),
518
+ title: '', // Title requires async, will be fetched separately
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Close a specific tab/page
524
+ */
525
+ async closeTab(index?: number): Promise<{ closed: number; remaining: number }> {
526
+ const targetIndex = index ?? this.activePageIndex;
527
+
528
+ if (targetIndex < 0 || targetIndex >= this.pages.length) {
529
+ throw new Error(`Invalid tab index: ${targetIndex}`);
530
+ }
531
+
532
+ if (this.pages.length === 1) {
533
+ throw new Error('Cannot close the last tab. Use "close" to close the browser.');
534
+ }
535
+
536
+ const page = this.pages[targetIndex];
537
+ await page.close();
538
+ this.pages.splice(targetIndex, 1);
539
+
540
+ // Adjust active index if needed
541
+ if (this.activePageIndex >= this.pages.length) {
542
+ this.activePageIndex = this.pages.length - 1;
543
+ } else if (this.activePageIndex > targetIndex) {
544
+ this.activePageIndex--;
545
+ }
546
+
547
+ return { closed: targetIndex, remaining: this.pages.length };
548
+ }
549
+
550
+ /**
551
+ * List all tabs with their info
552
+ */
553
+ async listTabs(): Promise<Array<{ index: number; url: string; title: string; active: boolean }>> {
554
+ const tabs = await Promise.all(
555
+ this.pages.map(async (page, index) => ({
556
+ index,
557
+ url: page.url(),
558
+ title: await page.title().catch(() => ''),
559
+ active: index === this.activePageIndex,
560
+ }))
561
+ );
562
+ return tabs;
563
+ }
564
+
565
+ /**
566
+ * Close the browser and clean up
567
+ */
568
+ async close(): Promise<void> {
569
+ for (const page of this.pages) {
570
+ await page.close().catch(() => {});
571
+ }
572
+ this.pages = [];
573
+
574
+ for (const context of this.contexts) {
575
+ await context.close().catch(() => {});
576
+ }
577
+ this.contexts = [];
578
+
579
+ if (this.browser) {
580
+ await this.browser.close().catch(() => {});
581
+ this.browser = null;
582
+ }
583
+
584
+ this.activePageIndex = 0;
585
+ }
586
+ }