@dyyz1993/agent-browser 0.9.2 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/__tests__/utils/parseCli.d.ts +1 -0
  2. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  3. package/dist/__tests__/utils/parseCli.js +18 -10
  4. package/dist/__tests__/utils/parseCli.js.map +1 -1
  5. package/dist/actions.d.ts.map +1 -1
  6. package/dist/actions.js +63 -3
  7. package/dist/actions.js.map +1 -1
  8. package/dist/browser.d.ts +46 -2
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +343 -13
  11. package/dist/browser.js.map +1 -1
  12. package/dist/cli/commands.d.ts.map +1 -1
  13. package/dist/cli/commands.js +8 -3
  14. package/dist/cli/commands.js.map +1 -1
  15. package/dist/cli/connection.d.ts.map +1 -1
  16. package/dist/cli/connection.js +39 -1
  17. package/dist/cli/connection.js.map +1 -1
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +27 -20
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/cli/output.d.ts.map +1 -1
  22. package/dist/cli/output.js +5 -0
  23. package/dist/cli/output.js.map +1 -1
  24. package/dist/cli.js +20 -0
  25. package/dist/cli.js.map +1 -1
  26. package/dist/daemon.d.ts.map +1 -1
  27. package/dist/daemon.js +147 -1
  28. package/dist/daemon.js.map +1 -1
  29. package/dist/message-bridge.d.ts.map +1 -1
  30. package/dist/message-bridge.js +22 -4
  31. package/dist/message-bridge.js.map +1 -1
  32. package/dist/openapi.d.ts +22 -0
  33. package/dist/openapi.d.ts.map +1 -0
  34. package/dist/openapi.js +382 -0
  35. package/dist/openapi.js.map +1 -0
  36. package/dist/protocol.d.ts.map +1 -1
  37. package/dist/protocol.js +18 -0
  38. package/dist/protocol.js.map +1 -1
  39. package/dist/recorder/inject.js +61 -134
  40. package/dist/stream-server-standalone.d.ts +10 -0
  41. package/dist/stream-server-standalone.d.ts.map +1 -1
  42. package/dist/stream-server-standalone.js +594 -74
  43. package/dist/stream-server-standalone.js.map +1 -1
  44. package/dist/stream-server.d.ts +67 -2
  45. package/dist/stream-server.d.ts.map +1 -1
  46. package/dist/stream-server.js +371 -51
  47. package/dist/stream-server.js.map +1 -1
  48. package/dist/swagger-ui.d.ts +6 -0
  49. package/dist/swagger-ui.d.ts.map +1 -0
  50. package/dist/swagger-ui.js +51 -0
  51. package/dist/swagger-ui.js.map +1 -0
  52. package/dist/test-live.d.ts +2 -0
  53. package/dist/test-live.d.ts.map +1 -0
  54. package/dist/test-live.js +333 -0
  55. package/dist/test-live.js.map +1 -0
  56. package/dist/types.d.ts +7 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js.map +1 -1
  59. package/dist/viewer-html.d.ts.map +1 -1
  60. package/dist/viewer-html.js +270 -58
  61. package/dist/viewer-html.js.map +1 -1
  62. package/dist/viewer-script.d.ts +20 -2
  63. package/dist/viewer-script.d.ts.map +1 -1
  64. package/dist/viewer-script.js +911 -154
  65. package/dist/viewer-script.js.map +1 -1
  66. package/package.json +1 -1
  67. package/scripts/postinstall.js +6 -32
  68. package/scripts/test-cli-help.sh +51 -0
  69. package/scripts/verify-form.sh +67 -0
  70. package/scripts/verify-login.sh +65 -0
  71. package/scripts/verify-recording.sh +80 -0
  72. package/scripts/verify-upload.sh +41 -0
  73. package/skills/agent-browser/SKILL.md +297 -160
  74. package/skills/agent-browser/references/commands.md +3 -0
  75. package/skills/agent-browser/references/mobile-viewer.md +188 -0
  76. package/skills/agent-browser/references/network-monitoring.md +232 -0
  77. package/skills/agent-browser/references/recorder.md +319 -0
  78. package/skills/agent-browser/references/viewer-mode.md +148 -0
  79. package/skills/agent-browser/templates/api-interception.sh +3 -1
  80. package/skills/agent-browser/templates/data-extraction.sh +8 -4
  81. package/skills/agent-browser/templates/form-automation.sh +18 -23
  82. package/skills/agent-browser/templates/network-intercept-crawl.sh +256 -0
  83. package/skills/agent-browser/templates/recorder-workflow.sh +51 -0
  84. package/skills/agent-browser/templates/viewer-remote.sh +41 -0
  85. package/dist/__tests__/test-iframe.d.ts +0 -2
  86. package/dist/__tests__/test-iframe.d.ts.map +0 -1
  87. package/dist/__tests__/test-iframe.js +0 -52
  88. package/dist/__tests__/test-iframe.js.map +0 -1
  89. package/dist/cli-new.d.ts +0 -3
  90. package/dist/cli-new.d.ts.map +0 -1
  91. package/dist/cli-new.js +0 -308
  92. package/dist/cli-new.js.map +0 -1
  93. package/dist/cli-old.d.ts +0 -3
  94. package/dist/cli-old.d.ts.map +0 -1
  95. package/dist/cli-old.js +0 -1101
  96. package/dist/cli-old.js.map +0 -1
  97. package/dist/recorder/binding.d.ts +0 -24
  98. package/dist/recorder/binding.d.ts.map +0 -1
  99. package/dist/recorder/binding.js +0 -215
  100. package/dist/recorder/binding.js.map +0 -1
  101. package/dist/recorder/index.d.ts +0 -4
  102. package/dist/recorder/index.d.ts.map +0 -1
  103. package/dist/recorder/index.js +0 -4
  104. package/dist/recorder/index.js.map +0 -1
  105. package/dist/recorder/recorder.d.ts +0 -19
  106. package/dist/recorder/recorder.d.ts.map +0 -1
  107. package/dist/recorder/recorder.js +0 -101
  108. package/dist/recorder/recorder.js.map +0 -1
  109. package/dist/recorder/store.d.ts +0 -22
  110. package/dist/recorder/store.d.ts.map +0 -1
  111. package/dist/recorder/store.js +0 -150
  112. package/dist/recorder/store.js.map +0 -1
  113. package/dist/recorder/types.d.ts +0 -73
  114. package/dist/recorder/types.d.ts.map +0 -1
  115. package/dist/recorder/types.js +0 -5
  116. package/dist/recorder/types.js.map +0 -1
package/dist/browser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { chromium, firefox, webkit, devices, } from 'playwright-core';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync, statSync } from 'node:fs';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  import { getEnhancedSnapshot, parseRef } from './snapshot.js';
@@ -24,6 +24,13 @@ export class BrowserManager {
24
24
  activePageIndex = 0;
25
25
  dialogHandler = null;
26
26
  trackedRequests = [];
27
+ isRequestTrackingEnabled = false;
28
+ isResponseCaptureEnabled = false;
29
+ // Map to track requests for response matching (instance variable for cross-listener access)
30
+ pendingRequests = new Map();
31
+ // Store request listener references for proper cleanup
32
+ requestListener = null;
33
+ responseListener = null;
27
34
  routes = new Map();
28
35
  consoleMessages = [];
29
36
  pageErrors = [];
@@ -252,27 +259,104 @@ export class BrowserManager {
252
259
  }
253
260
  /**
254
261
  * Start tracking requests
262
+ * @param captureResponse - Whether to capture response body (default: false for backward compatibility)
255
263
  */
256
- startRequestTracking() {
264
+ startRequestTracking(captureResponse = false) {
257
265
  const page = this.getPage();
258
- page.on('request', (request) => {
259
- this.trackedRequests.push({
266
+ // If already tracking with the same captureResponse setting, do nothing
267
+ if (this.isRequestTrackingEnabled && this.isResponseCaptureEnabled === captureResponse) {
268
+ return;
269
+ }
270
+ // Remove existing listeners if any
271
+ if (this.requestListener) {
272
+ page.off('request', this.requestListener);
273
+ }
274
+ if (this.responseListener) {
275
+ page.off('response', this.responseListener);
276
+ }
277
+ // Update flags
278
+ this.isRequestTrackingEnabled = true;
279
+ this.isResponseCaptureEnabled = captureResponse;
280
+ // Create request listener
281
+ this.requestListener = (request) => {
282
+ const trackedRequest = {
260
283
  url: request.url(),
261
284
  method: request.method(),
262
285
  headers: request.headers(),
263
286
  timestamp: Date.now(),
264
287
  resourceType: request.resourceType(),
265
- });
266
- });
288
+ };
289
+ // Store the request
290
+ this.trackedRequests.push(trackedRequest);
291
+ // Store for response matching
292
+ const key = `${request.url()}:${trackedRequest.timestamp}`;
293
+ this.pendingRequests.set(key, trackedRequest);
294
+ };
295
+ page.on('request', this.requestListener);
296
+ // Listen for response event (more reliable than request.response())
297
+ if (captureResponse) {
298
+ this.responseListener = async (response) => {
299
+ const request = response.request();
300
+ const url = request.url();
301
+ // Find the matching tracked request
302
+ for (const [key, trackedRequest] of this.pendingRequests.entries()) {
303
+ if (key.startsWith(url + ':')) {
304
+ trackedRequest.status = response.status();
305
+ trackedRequest.statusText = response.statusText();
306
+ trackedRequest.responseHeaders = response.headers();
307
+ trackedRequest.contentType = response.headers()['content-type'] || '';
308
+ // Try to get response body
309
+ try {
310
+ const body = await response.text();
311
+ // Try to parse as JSON if content-type indicates JSON
312
+ if (trackedRequest.contentType.includes('application/json') ||
313
+ trackedRequest.contentType.includes('text/json')) {
314
+ try {
315
+ trackedRequest.responseBody = JSON.parse(body);
316
+ }
317
+ catch {
318
+ trackedRequest.responseBody = body;
319
+ }
320
+ }
321
+ else {
322
+ trackedRequest.responseBody = body;
323
+ }
324
+ }
325
+ catch {
326
+ // Response body not available (e.g., for binary data or failed requests)
327
+ trackedRequest.responseBody = undefined;
328
+ }
329
+ // Remove from pending after processing
330
+ this.pendingRequests.delete(key);
331
+ break;
332
+ }
333
+ }
334
+ };
335
+ page.on('response', this.responseListener);
336
+ }
337
+ else {
338
+ this.responseListener = null;
339
+ }
267
340
  }
268
341
  /**
269
342
  * Get tracked requests
343
+ * @param filter - URL pattern to filter
344
+ * @param type - Filter by response type (e.g., 'json')
270
345
  */
271
- getRequests(filter) {
346
+ getRequests(filter, type) {
347
+ let requests = this.trackedRequests;
348
+ // Filter by URL pattern
272
349
  if (filter) {
273
- return this.trackedRequests.filter((r) => r.url.includes(filter));
350
+ requests = requests.filter((r) => r.url.includes(filter));
274
351
  }
275
- return this.trackedRequests;
352
+ // Filter by response type
353
+ if (type === 'json') {
354
+ requests = requests.filter((r) => {
355
+ const contentType = r.contentType || '';
356
+ return contentType.includes('application/json') || contentType.includes('text/json');
357
+ });
358
+ }
359
+ return requests;
276
360
  }
277
361
  /**
278
362
  * Clear tracked requests
@@ -280,6 +364,79 @@ export class BrowserManager {
280
364
  clearRequests() {
281
365
  this.trackedRequests = [];
282
366
  }
367
+ /**
368
+ * Save tracked requests to a directory
369
+ * @param outputDir - Directory path to save requests
370
+ * @param filter - URL pattern to filter
371
+ * @param type - Filter by response type (e.g., 'json')
372
+ * @returns Object with saved count and output path
373
+ */
374
+ saveRequestsToDir(outputDir, filter, type) {
375
+ // Get filtered requests
376
+ const requests = this.getRequests(filter, type);
377
+ // Resolve to absolute path
378
+ const absolutePath = path.resolve(outputDir);
379
+ // Check if path looks like a file (has extension and not already a directory)
380
+ const hasExtension = path.extname(absolutePath) !== '';
381
+ const isExistingDirectory = existsSync(absolutePath) && statSync(absolutePath).isDirectory();
382
+ // If path looks like a file and doesn't exist as directory, use parent directory
383
+ let targetPath = absolutePath;
384
+ let warningMessage;
385
+ if (hasExtension && !isExistingDirectory) {
386
+ // User specified a file path, use parent directory instead
387
+ targetPath = path.dirname(absolutePath);
388
+ warningMessage = `Warning: "${outputDir}" looks like a file path. Using directory: "${targetPath}"`;
389
+ console.warn(warningMessage);
390
+ }
391
+ // Create output directory if not exists
392
+ if (!existsSync(targetPath)) {
393
+ mkdirSync(targetPath, { recursive: true });
394
+ }
395
+ // Build index data
396
+ const indexData = {
397
+ capturedAt: new Date().toISOString(),
398
+ totalRequests: requests.length,
399
+ requests: [],
400
+ };
401
+ // Save each request to a separate file
402
+ requests.forEach((request, index) => {
403
+ const fileIndex = String(index + 1).padStart(3, '0');
404
+ // Generate filename from URL or use index
405
+ const urlObj = new URL(request.url);
406
+ const pathParts = urlObj.pathname.split('/').filter(Boolean);
407
+ const baseName = pathParts.length > 0 ? pathParts.join('_').substring(0, 50) : 'request';
408
+ const fileName = `${fileIndex}_${baseName}.json`;
409
+ const filePath = path.join(targetPath, fileName);
410
+ // Save individual request file
411
+ const requestData = {
412
+ url: request.url,
413
+ method: request.method,
414
+ status: request.status,
415
+ contentType: request.contentType,
416
+ timestamp: request.timestamp,
417
+ body: request.responseBody,
418
+ };
419
+ writeFileSync(filePath, JSON.stringify(requestData, null, 2), 'utf-8');
420
+ // Add to index
421
+ indexData.requests.push({
422
+ index: index + 1,
423
+ file: fileName,
424
+ url: request.url,
425
+ method: request.method,
426
+ status: request.status,
427
+ contentType: request.contentType,
428
+ timestamp: request.timestamp,
429
+ });
430
+ });
431
+ // Save index file
432
+ const indexPath = path.join(targetPath, 'index.json');
433
+ writeFileSync(indexPath, JSON.stringify(indexData, null, 2), 'utf-8');
434
+ return {
435
+ savedCount: requests.length,
436
+ outputPath: targetPath,
437
+ indexPath,
438
+ };
439
+ }
283
440
  /**
284
441
  * Add a route to intercept requests
285
442
  */
@@ -986,16 +1143,15 @@ export class BrowserManager {
986
1143
  ? ['--allow-file-access-from-files', '--allow-file-access']
987
1144
  : [];
988
1145
  // Add anti-detection args
1146
+ const isHeaded = hasExtensions || options.headless === false;
989
1147
  const antiDetectionArgs = [
990
1148
  '--disable-blink-features=AutomationControlled',
991
1149
  '--disable-dev-shm-usage',
992
1150
  '--no-sandbox',
993
- '--disable-gpu',
994
- '--disable-software-rasterizer',
1151
+ ...(isHeaded ? [] : ['--disable-gpu']),
995
1152
  '--enable-features=WebGL',
996
1153
  '--ignore-gpu-blacklist',
997
- '--use-gl=desktop',
998
- '--enable-gpu-compositing',
1154
+ ...(isHeaded ? ['--use-gl=desktop', '--enable-gpu-compositing'] : []),
999
1155
  ];
1000
1156
  const baseArgs = options.args
1001
1157
  ? [...fileAccessArgs, ...antiDetectionArgs, ...options.args]
@@ -1565,6 +1721,162 @@ export class BrowserManager {
1565
1721
  const cdp = await this.getCDPSession();
1566
1722
  await cdp.send('Input.insertText', { text });
1567
1723
  }
1724
+ _lastFillSelector = '';
1725
+ _lastFillValue = '';
1726
+ _fillFocusedSelector = '';
1727
+ async fillValue(selector, value) {
1728
+ const page = this.getPage();
1729
+ if (!page)
1730
+ return;
1731
+ if (!selector || value === undefined)
1732
+ return;
1733
+ if (value === this._lastFillValue && selector === this._lastFillSelector)
1734
+ return;
1735
+ this._lastFillSelector = selector;
1736
+ this._lastFillValue = value;
1737
+ const needsFocus = !this._fillFocusedSelector || this._fillFocusedSelector !== selector;
1738
+ await page.evaluate(({ selector, value, needsFocus }) => {
1739
+ const el = document.querySelector(selector);
1740
+ if (!el)
1741
+ return { ok: false, reason: 'not_found' };
1742
+ const isContentEditable = el instanceof HTMLElement &&
1743
+ (el.isContentEditable || el.getAttribute('contenteditable') === 'true');
1744
+ if (needsFocus && !isContentEditable) {
1745
+ el.focus();
1746
+ }
1747
+ if (isContentEditable) {
1748
+ if (needsFocus)
1749
+ el.focus();
1750
+ document.execCommand('selectAll', false, undefined);
1751
+ document.execCommand('insertText', false, value);
1752
+ return { ok: true, method: 'contenteditable' };
1753
+ }
1754
+ const tag = el.tagName.toLowerCase();
1755
+ const isInput = tag === 'input';
1756
+ const isTextarea = tag === 'textarea';
1757
+ if (!isInput && !isTextarea) {
1758
+ return { ok: false, reason: 'not_input' };
1759
+ }
1760
+ const proto = isInput
1761
+ ? window.HTMLInputElement.prototype
1762
+ : window.HTMLTextAreaElement.prototype;
1763
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
1764
+ if (nativeInputValueSetter) {
1765
+ nativeInputValueSetter.call(el, value);
1766
+ }
1767
+ else {
1768
+ el.value = value;
1769
+ }
1770
+ el.dispatchEvent(new InputEvent('input', {
1771
+ bubbles: true,
1772
+ cancelable: true,
1773
+ inputType: 'insertReplacementText',
1774
+ data: value,
1775
+ }));
1776
+ return { ok: true, method: 'native_setter' };
1777
+ }, { selector, value, needsFocus });
1778
+ if (needsFocus) {
1779
+ this._fillFocusedSelector = selector;
1780
+ }
1781
+ }
1782
+ clearFillState(selector) {
1783
+ if (selector && this._fillFocusedSelector === selector) {
1784
+ this._fillFocusedSelector = '';
1785
+ }
1786
+ if (!selector) {
1787
+ this._fillFocusedSelector = '';
1788
+ this._lastFillSelector = '';
1789
+ this._lastFillValue = '';
1790
+ }
1791
+ }
1792
+ async blurElement(selector) {
1793
+ const page = this.getPage();
1794
+ if (!page)
1795
+ return;
1796
+ await page.evaluate((sel) => {
1797
+ const el = document.querySelector(sel);
1798
+ if (el)
1799
+ el.blur();
1800
+ }, selector);
1801
+ }
1802
+ /**
1803
+ * Press a key on the page via Playwright.
1804
+ */
1805
+ async pressKey(key) {
1806
+ const page = this.getPage();
1807
+ if (!page)
1808
+ return;
1809
+ await page.keyboard.press(key);
1810
+ }
1811
+ /**
1812
+ * Inject focus/input/blur event listeners into the remote page.
1813
+ * Uses Playwright exposeFunction + addInitScript so the
1814
+ * injected script can call back to Node.js when input elements are focused.
1815
+ */
1816
+ async injectFocusListener(onEvent) {
1817
+ const page = this.getPage();
1818
+ if (!page)
1819
+ return;
1820
+ try {
1821
+ await page.exposeFunction('__agentBrowserInputEvent', (data) => {
1822
+ onEvent(data);
1823
+ });
1824
+ }
1825
+ catch {
1826
+ // Already registered from previous injection - safe to continue
1827
+ }
1828
+ const injectScript = `
1829
+ (function() {
1830
+ if (window.__agentBrowserListenerInjected) return;
1831
+ window.__agentBrowserListenerInjected = true;
1832
+
1833
+ document.addEventListener('focus', function(e) {
1834
+ var el = e.target;
1835
+ if (!el) return;
1836
+ var tag = el.tagName;
1837
+ if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !el.isContentEditable) return;
1838
+ try {
1839
+ window.__agentBrowserInputEvent({
1840
+ type: 'input_focused',
1841
+ tag: tag,
1842
+ inputType: el.type || '',
1843
+ value: typeof el.value === 'string' ? el.value : '',
1844
+ placeholder: el.placeholder || '',
1845
+ id: el.id || '',
1846
+ selector: (function() {
1847
+ if (el.id) return '#' + el.id;
1848
+ if (el.name && el.name) return '[name="' + el.name + '"]';
1849
+ return el.tagName.toLowerCase();
1850
+ })()
1851
+ });
1852
+ } catch(ex) {}
1853
+ }, true);
1854
+
1855
+ document.addEventListener('input', function(e) {
1856
+ var el = e.target;
1857
+ if (!el) return;
1858
+ var tag = el.tagName;
1859
+ if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !el.isContentEditable) return;
1860
+ try {
1861
+ window.__agentBrowserInputEvent({
1862
+ type: 'input_value',
1863
+ text: typeof el.value === 'string' ? el.value : ''
1864
+ });
1865
+ } catch(ex) {}
1866
+ }, true);
1867
+
1868
+ document.addEventListener('blur', function() {
1869
+ try {
1870
+ window.__agentBrowserInputEvent({ type: 'input_blur' });
1871
+ } catch(ex) {}
1872
+ }, true);
1873
+ })();
1874
+ `;
1875
+ // Inject into future navigations
1876
+ await page.addInitScript(injectScript);
1877
+ // Also inject into current page (already loaded)
1878
+ await page.evaluate(injectScript);
1879
+ }
1568
1880
  /**
1569
1881
  * Check if video recording is currently active
1570
1882
  */
@@ -2510,6 +2822,24 @@ export class BrowserManager {
2510
2822
  this.recorderPageHandler = null;
2511
2823
  }
2512
2824
  }
2825
+ // Clean up network tracking state and listeners
2826
+ if (page) {
2827
+ if (this.requestListener) {
2828
+ page.off('request', this.requestListener);
2829
+ this.requestListener = null;
2830
+ }
2831
+ if (this.responseListener) {
2832
+ page.off('response', this.responseListener);
2833
+ this.responseListener = null;
2834
+ }
2835
+ }
2836
+ this.trackedRequests = [];
2837
+ this.pendingRequests.clear();
2838
+ this.isRequestTrackingEnabled = false;
2839
+ this.isResponseCaptureEnabled = false;
2840
+ this.routes.clear();
2841
+ this.consoleMessages = [];
2842
+ this.pageErrors = [];
2513
2843
  // Clean up navigation state
2514
2844
  this.navigationHistory = [];
2515
2845
  this.navigationHistoryIndex = -1;