agent-browser 0.5.0 → 0.7.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 (43) hide show
  1. package/README.md +109 -9
  2. package/bin/agent-browser-linux-x64 +0 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +97 -13
  5. package/dist/actions.js.map +1 -1
  6. package/dist/browser.d.ts +62 -1
  7. package/dist/browser.d.ts.map +1 -1
  8. package/dist/browser.js +423 -24
  9. package/dist/browser.js.map +1 -1
  10. package/dist/daemon.d.ts +5 -0
  11. package/dist/daemon.d.ts.map +1 -1
  12. package/dist/daemon.js +52 -6
  13. package/dist/daemon.js.map +1 -1
  14. package/dist/protocol.d.ts.map +1 -1
  15. package/dist/protocol.js +47 -1
  16. package/dist/protocol.js.map +1 -1
  17. package/dist/types.d.ts +69 -1
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +26 -23
  20. package/scripts/build-all-platforms.sh +0 -0
  21. package/scripts/copy-native.js +2 -1
  22. package/scripts/postinstall.js +1 -1
  23. package/skills/agent-browser/SKILL.md +221 -17
  24. package/skills/agent-browser/references/authentication.md +188 -0
  25. package/skills/agent-browser/references/proxy-support.md +175 -0
  26. package/skills/agent-browser/references/session-management.md +181 -0
  27. package/skills/agent-browser/references/snapshot-refs.md +186 -0
  28. package/skills/agent-browser/references/video-recording.md +162 -0
  29. package/skills/agent-browser/templates/authenticated-session.sh +91 -0
  30. package/skills/agent-browser/templates/capture-workflow.sh +68 -0
  31. package/skills/agent-browser/templates/form-automation.sh +64 -0
  32. package/bin/agent-browser-darwin-arm64 +0 -0
  33. package/bin/agent-browser-darwin-x64 +0 -0
  34. package/bin/agent-browser-linux-arm64 +0 -0
  35. package/bin/agent-browser-win32-x64.exe +0 -0
  36. package/dist/browser.test.d.ts +0 -2
  37. package/dist/browser.test.d.ts.map +0 -1
  38. package/dist/browser.test.js +0 -136
  39. package/dist/browser.test.js.map +0 -1
  40. package/dist/protocol.test.d.ts +0 -2
  41. package/dist/protocol.test.d.ts.map +0 -1
  42. package/dist/protocol.test.js +0 -176
  43. package/dist/protocol.test.js.map +0 -1
package/dist/browser.js CHANGED
@@ -1,14 +1,19 @@
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 } from 'node:fs';
4
5
  import { getEnhancedSnapshot, parseRef } from './snapshot.js';
5
6
  /**
6
7
  * Manages the Playwright browser lifecycle with multiple tabs/windows
7
8
  */
8
9
  export class BrowserManager {
9
10
  browser = null;
10
- cdpPort = null;
11
+ cdpEndpoint = null; // stores port number or full URL
11
12
  isPersistentContext = false;
13
+ browserbaseSessionId = null;
14
+ browserbaseApiKey = null;
15
+ browserUseSessionId = null;
16
+ browserUseApiKey = null;
12
17
  contexts = [];
13
18
  pages = [];
14
19
  activePageIndex = 0;
@@ -28,6 +33,11 @@ export class BrowserManager {
28
33
  screencastSessionId = 0;
29
34
  frameCallback = null;
30
35
  screencastFrameHandler = null;
36
+ // Video recording (Playwright native)
37
+ recordingContext = null;
38
+ recordingPage = null;
39
+ recordingOutputPath = '';
40
+ recordingTempDir = '';
31
41
  /**
32
42
  * Check if browser is launched
33
43
  */
@@ -507,27 +517,174 @@ export class BrowserManager {
507
517
  /**
508
518
  * Check if CDP connection needs to be re-established
509
519
  */
510
- needsCdpReconnect(cdpPort) {
520
+ needsCdpReconnect(cdpEndpoint) {
511
521
  if (!this.browser?.isConnected())
512
522
  return true;
513
- if (this.cdpPort !== cdpPort)
523
+ if (this.cdpEndpoint !== cdpEndpoint)
514
524
  return true;
515
525
  if (!this.isCdpConnectionAlive())
516
526
  return true;
517
527
  return false;
518
528
  }
529
+ /**
530
+ * Close a Browserbase session via API
531
+ */
532
+ async closeBrowserbaseSession(sessionId, apiKey) {
533
+ await fetch(`https://api.browserbase.com/v1/sessions/${sessionId}`, {
534
+ method: 'DELETE',
535
+ headers: {
536
+ 'X-BB-API-Key': apiKey,
537
+ },
538
+ });
539
+ }
540
+ /**
541
+ * Close a Browser Use session via API
542
+ */
543
+ async closeBrowserUseSession(sessionId, apiKey) {
544
+ const response = await fetch(`https://api.browser-use.com/api/v2/browsers/${sessionId}`, {
545
+ method: 'PATCH',
546
+ headers: {
547
+ 'Content-Type': 'application/json',
548
+ 'X-Browser-Use-API-Key': apiKey,
549
+ },
550
+ body: JSON.stringify({ action: 'stop' }),
551
+ });
552
+ if (!response.ok) {
553
+ throw new Error(`Failed to close Browser Use session: ${response.statusText}`);
554
+ }
555
+ }
556
+ /**
557
+ * Connect to Browserbase remote browser via CDP.
558
+ * Requires BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID environment variables.
559
+ */
560
+ async connectToBrowserbase() {
561
+ const browserbaseApiKey = process.env.BROWSERBASE_API_KEY;
562
+ const browserbaseProjectId = process.env.BROWSERBASE_PROJECT_ID;
563
+ if (!browserbaseApiKey || !browserbaseProjectId) {
564
+ throw new Error('BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are required when using browserbase as a provider');
565
+ }
566
+ const response = await fetch('https://api.browserbase.com/v1/sessions', {
567
+ method: 'POST',
568
+ headers: {
569
+ 'Content-Type': 'application/json',
570
+ 'X-BB-API-Key': browserbaseApiKey,
571
+ },
572
+ body: JSON.stringify({
573
+ projectId: browserbaseProjectId,
574
+ }),
575
+ });
576
+ if (!response.ok) {
577
+ throw new Error(`Failed to create Browserbase session: ${response.statusText}`);
578
+ }
579
+ const session = (await response.json());
580
+ const browser = await chromium.connectOverCDP(session.connectUrl).catch(() => {
581
+ throw new Error('Failed to connect to Browserbase session via CDP');
582
+ });
583
+ try {
584
+ const contexts = browser.contexts();
585
+ if (contexts.length === 0) {
586
+ throw new Error('No browser context found in Browserbase session');
587
+ }
588
+ const context = contexts[0];
589
+ const pages = context.pages();
590
+ const page = pages[0] ?? (await context.newPage());
591
+ this.browserbaseSessionId = session.id;
592
+ this.browserbaseApiKey = browserbaseApiKey;
593
+ this.browser = browser;
594
+ context.setDefaultTimeout(10000);
595
+ this.contexts.push(context);
596
+ this.pages.push(page);
597
+ this.activePageIndex = 0;
598
+ this.setupPageTracking(page);
599
+ }
600
+ catch (error) {
601
+ await this.closeBrowserbaseSession(session.id, browserbaseApiKey).catch((sessionError) => {
602
+ console.error('Failed to close Browserbase session during cleanup:', sessionError);
603
+ });
604
+ throw error;
605
+ }
606
+ }
607
+ /**
608
+ * Connect to Browser Use remote browser via CDP.
609
+ * Requires BROWSER_USE_API_KEY environment variable.
610
+ */
611
+ async connectToBrowserUse() {
612
+ const browserUseApiKey = process.env.BROWSER_USE_API_KEY;
613
+ if (!browserUseApiKey) {
614
+ throw new Error('BROWSER_USE_API_KEY is required when using browseruse as a provider');
615
+ }
616
+ const response = await fetch('https://api.browser-use.com/api/v2/browsers', {
617
+ method: 'POST',
618
+ headers: {
619
+ 'Content-Type': 'application/json',
620
+ 'X-Browser-Use-API-Key': browserUseApiKey,
621
+ },
622
+ body: JSON.stringify({}),
623
+ });
624
+ if (!response.ok) {
625
+ throw new Error(`Failed to create Browser Use session: ${response.statusText}`);
626
+ }
627
+ let session;
628
+ try {
629
+ session = (await response.json());
630
+ }
631
+ catch (error) {
632
+ throw new Error(`Failed to parse Browser Use session response: ${error instanceof Error ? error.message : String(error)}`);
633
+ }
634
+ if (!session.id || !session.cdpUrl) {
635
+ throw new Error(`Invalid Browser Use session response: missing ${!session.id ? 'id' : 'cdpUrl'}`);
636
+ }
637
+ const browser = await chromium.connectOverCDP(session.cdpUrl).catch(() => {
638
+ throw new Error('Failed to connect to Browser Use session via CDP');
639
+ });
640
+ try {
641
+ const contexts = browser.contexts();
642
+ let context;
643
+ let page;
644
+ if (contexts.length === 0) {
645
+ context = await browser.newContext();
646
+ page = await context.newPage();
647
+ }
648
+ else {
649
+ context = contexts[0];
650
+ const pages = context.pages();
651
+ page = pages[0] ?? (await context.newPage());
652
+ }
653
+ this.browserUseSessionId = session.id;
654
+ this.browserUseApiKey = browserUseApiKey;
655
+ this.browser = browser;
656
+ context.setDefaultTimeout(60000);
657
+ this.contexts.push(context);
658
+ this.pages.push(page);
659
+ this.activePageIndex = 0;
660
+ this.setupPageTracking(page);
661
+ this.setupContextTracking(context);
662
+ }
663
+ catch (error) {
664
+ await this.closeBrowserUseSession(session.id, browserUseApiKey).catch((sessionError) => {
665
+ console.error('Failed to close Browser Use session during cleanup:', sessionError);
666
+ });
667
+ throw error;
668
+ }
669
+ }
519
670
  /**
520
671
  * Launch the browser with the specified options
521
672
  * If already launched, this is a no-op (browser stays open)
522
673
  */
523
674
  async launch(options) {
524
- const cdpPort = options.cdpPort;
675
+ // Determine CDP endpoint: prefer cdpUrl over cdpPort for flexibility
676
+ const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined);
525
677
  const hasExtensions = !!options.extensions?.length;
526
- if (hasExtensions && cdpPort) {
678
+ const hasProfile = !!options.profile;
679
+ if (hasExtensions && cdpEndpoint) {
527
680
  throw new Error('Extensions cannot be used with CDP connection');
528
681
  }
682
+ if (hasProfile && cdpEndpoint) {
683
+ throw new Error('Profile cannot be used with CDP connection');
684
+ }
529
685
  if (this.isLaunched()) {
530
- const needsRelaunch = (!cdpPort && this.cdpPort !== null) || (!!cdpPort && this.needsCdpReconnect(cdpPort));
686
+ const needsRelaunch = (!cdpEndpoint && this.cdpEndpoint !== null) ||
687
+ (!!cdpEndpoint && this.needsCdpReconnect(cdpEndpoint));
531
688
  if (needsRelaunch) {
532
689
  await this.close();
533
690
  }
@@ -535,8 +692,19 @@ export class BrowserManager {
535
692
  return;
536
693
  }
537
694
  }
538
- if (cdpPort) {
539
- await this.connectViaCDP(cdpPort);
695
+ if (cdpEndpoint) {
696
+ await this.connectViaCDP(cdpEndpoint);
697
+ return;
698
+ }
699
+ // Cloud browser providers require explicit opt-in via -p flag or AGENT_BROWSER_PROVIDER env var
700
+ // -p flag takes precedence over env var
701
+ const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER;
702
+ if (provider === 'browserbase') {
703
+ await this.connectToBrowserbase();
704
+ return;
705
+ }
706
+ if (provider === 'browseruse') {
707
+ await this.connectToBrowserUse();
540
708
  return;
541
709
  }
542
710
  const browserType = options.browser ?? 'chromium';
@@ -547,26 +715,51 @@ export class BrowserManager {
547
715
  const viewport = options.viewport ?? { width: 1280, height: 720 };
548
716
  let context;
549
717
  if (hasExtensions) {
718
+ // Extensions require persistent context in a temp directory
550
719
  const extPaths = options.extensions.join(',');
551
720
  const session = process.env.AGENT_BROWSER_SESSION || 'default';
721
+ // Combine extension args with custom args
722
+ const extArgs = [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`];
723
+ const allArgs = options.args ? [...extArgs, ...options.args] : extArgs;
552
724
  context = await launcher.launchPersistentContext(path.join(os.tmpdir(), `agent-browser-ext-${session}`), {
553
725
  headless: false,
554
726
  executablePath: options.executablePath,
555
- args: [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`],
727
+ args: allArgs,
728
+ viewport,
729
+ extraHTTPHeaders: options.headers,
730
+ userAgent: options.userAgent,
731
+ ...(options.proxy && { proxy: options.proxy }),
732
+ });
733
+ this.isPersistentContext = true;
734
+ }
735
+ else if (hasProfile) {
736
+ // Profile uses persistent context for durable cookies/storage
737
+ // Expand ~ to home directory since it won't be shell-expanded
738
+ const profilePath = options.profile.replace(/^~\//, os.homedir() + '/');
739
+ context = await launcher.launchPersistentContext(profilePath, {
740
+ headless: options.headless ?? true,
741
+ executablePath: options.executablePath,
556
742
  viewport,
557
743
  extraHTTPHeaders: options.headers,
558
744
  });
559
745
  this.isPersistentContext = true;
560
746
  }
561
747
  else {
748
+ // Regular ephemeral browser
562
749
  this.browser = await launcher.launch({
563
750
  headless: options.headless ?? true,
564
751
  executablePath: options.executablePath,
752
+ args: options.args,
753
+ });
754
+ this.cdpEndpoint = null;
755
+ context = await this.browser.newContext({
756
+ viewport,
757
+ extraHTTPHeaders: options.headers,
758
+ userAgent: options.userAgent,
759
+ ...(options.proxy && { proxy: options.proxy }),
565
760
  });
566
- this.cdpPort = null;
567
- context = await this.browser.newContext({ viewport, extraHTTPHeaders: options.headers });
568
761
  }
569
- context.setDefaultTimeout(10000);
762
+ context.setDefaultTimeout(60000);
570
763
  this.contexts.push(context);
571
764
  const page = context.pages()[0] ?? (await context.newPage());
572
765
  this.pages.push(page);
@@ -575,14 +768,36 @@ export class BrowserManager {
575
768
  }
576
769
  /**
577
770
  * Connect to a running browser via CDP (Chrome DevTools Protocol)
771
+ * @param cdpEndpoint Either a port number (as string) or a full WebSocket URL (ws:// or wss://)
578
772
  */
579
- async connectViaCDP(cdpPort) {
580
- if (!cdpPort) {
581
- throw new Error('cdpPort is required for CDP connection');
773
+ async connectViaCDP(cdpEndpoint) {
774
+ if (!cdpEndpoint) {
775
+ throw new Error('CDP endpoint is required for CDP connection');
582
776
  }
583
- const browser = await chromium.connectOverCDP(`http://localhost:${cdpPort}`).catch(() => {
584
- throw new Error(`Failed to connect via CDP on port ${cdpPort}. ` +
585
- `Make sure the app is running with --remote-debugging-port=${cdpPort}`);
777
+ // Determine the connection URL:
778
+ // - If it starts with ws://, wss://, http://, or https://, use it directly
779
+ // - If it's a numeric string (e.g., "9222"), treat as port for localhost
780
+ // - Otherwise, treat it as a port number for localhost
781
+ let cdpUrl;
782
+ if (cdpEndpoint.startsWith('ws://') ||
783
+ cdpEndpoint.startsWith('wss://') ||
784
+ cdpEndpoint.startsWith('http://') ||
785
+ cdpEndpoint.startsWith('https://')) {
786
+ cdpUrl = cdpEndpoint;
787
+ }
788
+ else if (/^\d+$/.test(cdpEndpoint)) {
789
+ // Numeric string - treat as port number (handles JSON serialization quirks)
790
+ cdpUrl = `http://localhost:${cdpEndpoint}`;
791
+ }
792
+ else {
793
+ // Unknown format - still try as port for backward compatibility
794
+ cdpUrl = `http://localhost:${cdpEndpoint}`;
795
+ }
796
+ const browser = await chromium.connectOverCDP(cdpUrl).catch(() => {
797
+ throw new Error(`Failed to connect via CDP to ${cdpUrl}. ` +
798
+ (cdpUrl.includes('localhost')
799
+ ? `Make sure the app is running with --remote-debugging-port=${cdpEndpoint}`
800
+ : 'Make sure the remote browser is accessible and the URL is correct.'));
586
801
  });
587
802
  // Validate and set up state, cleaning up browser connection if anything fails
588
803
  try {
@@ -590,13 +805,14 @@ export class BrowserManager {
590
805
  if (contexts.length === 0) {
591
806
  throw new Error('No browser context found. Make sure the app has an open window.');
592
807
  }
593
- const allPages = contexts.flatMap((context) => context.pages());
808
+ // Filter out pages with empty URLs, which can cause Playwright to hang
809
+ const allPages = contexts.flatMap((context) => context.pages()).filter((page) => page.url());
594
810
  if (allPages.length === 0) {
595
811
  throw new Error('No page found. Make sure the app has loaded content.');
596
812
  }
597
813
  // All validation passed - commit state
598
814
  this.browser = browser;
599
- this.cdpPort = cdpPort;
815
+ this.cdpEndpoint = cdpEndpoint;
600
816
  for (const context of contexts) {
601
817
  this.contexts.push(context);
602
818
  this.setupContextTracking(context);
@@ -676,7 +892,7 @@ export class BrowserManager {
676
892
  const context = await this.browser.newContext({
677
893
  viewport: viewport ?? { width: 1280, height: 720 },
678
894
  });
679
- context.setDefaultTimeout(10000);
895
+ context.setDefaultTimeout(60000);
680
896
  this.contexts.push(context);
681
897
  const page = await context.newPage();
682
898
  this.pages.push(page);
@@ -888,10 +1104,177 @@ export class BrowserManager {
888
1104
  modifiers: params.modifiers ?? 0,
889
1105
  });
890
1106
  }
1107
+ /**
1108
+ * Check if video recording is currently active
1109
+ */
1110
+ isRecording() {
1111
+ return this.recordingContext !== null;
1112
+ }
1113
+ /**
1114
+ * Start recording to a video file using Playwright's native video recording.
1115
+ * Creates a fresh browser context with video recording enabled.
1116
+ * Automatically captures current URL and transfers cookies/storage if no URL provided.
1117
+ *
1118
+ * @param outputPath - Path to the output video file (will be .webm)
1119
+ * @param url - Optional URL to navigate to (defaults to current page URL)
1120
+ */
1121
+ async startRecording(outputPath, url) {
1122
+ if (this.recordingContext) {
1123
+ throw new Error("Recording already in progress. Run 'record stop' first, or use 'record restart' to stop and start a new recording.");
1124
+ }
1125
+ if (!this.browser) {
1126
+ throw new Error('Browser not launched. Call launch first.');
1127
+ }
1128
+ // Check if output file already exists
1129
+ if (existsSync(outputPath)) {
1130
+ throw new Error(`Output file already exists: ${outputPath}`);
1131
+ }
1132
+ // Validate output path is .webm (Playwright native format)
1133
+ if (!outputPath.endsWith('.webm')) {
1134
+ throw new Error('Playwright native recording only supports WebM format. Please use a .webm extension.');
1135
+ }
1136
+ // Auto-capture current URL if none provided
1137
+ const currentPage = this.pages.length > 0 ? this.pages[this.activePageIndex] : null;
1138
+ const currentContext = this.contexts.length > 0 ? this.contexts[0] : null;
1139
+ if (!url && currentPage) {
1140
+ const currentUrl = currentPage.url();
1141
+ if (currentUrl && currentUrl !== 'about:blank') {
1142
+ url = currentUrl;
1143
+ }
1144
+ }
1145
+ // Capture state from current context (cookies + storage)
1146
+ let storageState;
1147
+ if (currentContext) {
1148
+ try {
1149
+ storageState = await currentContext.storageState();
1150
+ }
1151
+ catch {
1152
+ // Ignore errors - context might be closed or invalid
1153
+ }
1154
+ }
1155
+ // Create a temp directory for video recording
1156
+ const session = process.env.AGENT_BROWSER_SESSION || 'default';
1157
+ this.recordingTempDir = path.join(os.tmpdir(), `agent-browser-recording-${session}-${Date.now()}`);
1158
+ mkdirSync(this.recordingTempDir, { recursive: true });
1159
+ this.recordingOutputPath = outputPath;
1160
+ // Create a new context with video recording enabled and restored state
1161
+ const viewport = { width: 1280, height: 720 };
1162
+ this.recordingContext = await this.browser.newContext({
1163
+ viewport,
1164
+ recordVideo: {
1165
+ dir: this.recordingTempDir,
1166
+ size: viewport,
1167
+ },
1168
+ storageState,
1169
+ });
1170
+ this.recordingContext.setDefaultTimeout(10000);
1171
+ // Create a page in the recording context
1172
+ this.recordingPage = await this.recordingContext.newPage();
1173
+ // Add the recording context and page to our managed lists
1174
+ this.contexts.push(this.recordingContext);
1175
+ this.pages.push(this.recordingPage);
1176
+ this.activePageIndex = this.pages.length - 1;
1177
+ // Set up page tracking
1178
+ this.setupPageTracking(this.recordingPage);
1179
+ // Invalidate CDP session since we switched pages
1180
+ await this.invalidateCDPSession();
1181
+ // Navigate to URL if provided or captured
1182
+ if (url) {
1183
+ await this.recordingPage.goto(url, { waitUntil: 'load' });
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Stop recording and save the video file
1188
+ * @returns Recording result with path
1189
+ */
1190
+ async stopRecording() {
1191
+ if (!this.recordingContext || !this.recordingPage) {
1192
+ return { path: '', frames: 0, error: 'No recording in progress' };
1193
+ }
1194
+ const outputPath = this.recordingOutputPath;
1195
+ try {
1196
+ // Get the video object before closing the page
1197
+ const video = this.recordingPage.video();
1198
+ // Remove recording page/context from our managed lists before closing
1199
+ const pageIndex = this.pages.indexOf(this.recordingPage);
1200
+ if (pageIndex !== -1) {
1201
+ this.pages.splice(pageIndex, 1);
1202
+ }
1203
+ const contextIndex = this.contexts.indexOf(this.recordingContext);
1204
+ if (contextIndex !== -1) {
1205
+ this.contexts.splice(contextIndex, 1);
1206
+ }
1207
+ // Close the page to finalize the video
1208
+ await this.recordingPage.close();
1209
+ // Save the video to the desired output path
1210
+ if (video) {
1211
+ await video.saveAs(outputPath);
1212
+ }
1213
+ // Clean up temp directory
1214
+ if (this.recordingTempDir) {
1215
+ rmSync(this.recordingTempDir, { recursive: true, force: true });
1216
+ }
1217
+ // Close the recording context
1218
+ await this.recordingContext.close();
1219
+ // Reset recording state
1220
+ this.recordingContext = null;
1221
+ this.recordingPage = null;
1222
+ this.recordingOutputPath = '';
1223
+ this.recordingTempDir = '';
1224
+ // Adjust active page index
1225
+ if (this.pages.length > 0) {
1226
+ this.activePageIndex = Math.min(this.activePageIndex, this.pages.length - 1);
1227
+ }
1228
+ else {
1229
+ this.activePageIndex = 0;
1230
+ }
1231
+ // Invalidate CDP session since we may have switched pages
1232
+ await this.invalidateCDPSession();
1233
+ return { path: outputPath, frames: 0 }; // Playwright doesn't expose frame count
1234
+ }
1235
+ catch (error) {
1236
+ // Clean up temp directory on error
1237
+ if (this.recordingTempDir) {
1238
+ rmSync(this.recordingTempDir, { recursive: true, force: true });
1239
+ }
1240
+ // Reset state on error
1241
+ this.recordingContext = null;
1242
+ this.recordingPage = null;
1243
+ this.recordingOutputPath = '';
1244
+ this.recordingTempDir = '';
1245
+ const message = error instanceof Error ? error.message : String(error);
1246
+ return { path: outputPath, frames: 0, error: message };
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Restart recording - stops current recording (if any) and starts a new one.
1251
+ * Convenience method that combines stopRecording and startRecording.
1252
+ *
1253
+ * @param outputPath - Path to the output video file (must be .webm)
1254
+ * @param url - Optional URL to navigate to (defaults to current page URL)
1255
+ * @returns Result from stopping the previous recording (if any)
1256
+ */
1257
+ async restartRecording(outputPath, url) {
1258
+ let previousPath;
1259
+ let stopped = false;
1260
+ // Stop current recording if active
1261
+ if (this.recordingContext) {
1262
+ const result = await this.stopRecording();
1263
+ previousPath = result.path;
1264
+ stopped = true;
1265
+ }
1266
+ // Start new recording
1267
+ await this.startRecording(outputPath, url);
1268
+ return { previousPath, stopped };
1269
+ }
891
1270
  /**
892
1271
  * Close the browser and clean up
893
1272
  */
894
1273
  async close() {
1274
+ // Stop recording if active (saves video)
1275
+ if (this.recordingContext) {
1276
+ await this.stopRecording();
1277
+ }
895
1278
  // Stop screencast if active
896
1279
  if (this.screencastActive) {
897
1280
  await this.stopScreencast();
@@ -901,8 +1284,20 @@ export class BrowserManager {
901
1284
  await this.cdpSession.detach().catch(() => { });
902
1285
  this.cdpSession = null;
903
1286
  }
904
- // CDP: only disconnect, don't close external app's pages
905
- if (this.cdpPort !== null) {
1287
+ if (this.browserbaseSessionId && this.browserbaseApiKey) {
1288
+ await this.closeBrowserbaseSession(this.browserbaseSessionId, this.browserbaseApiKey).catch((error) => {
1289
+ console.error('Failed to close Browserbase session:', error);
1290
+ });
1291
+ this.browser = null;
1292
+ }
1293
+ else if (this.browserUseSessionId && this.browserUseApiKey) {
1294
+ await this.closeBrowserUseSession(this.browserUseSessionId, this.browserUseApiKey).catch((error) => {
1295
+ console.error('Failed to close Browser Use session:', error);
1296
+ });
1297
+ this.browser = null;
1298
+ }
1299
+ else if (this.cdpEndpoint !== null) {
1300
+ // CDP: only disconnect, don't close external app's pages
906
1301
  if (this.browser) {
907
1302
  await this.browser.close().catch(() => { });
908
1303
  this.browser = null;
@@ -923,7 +1318,11 @@ export class BrowserManager {
923
1318
  }
924
1319
  this.pages = [];
925
1320
  this.contexts = [];
926
- this.cdpPort = null;
1321
+ this.cdpEndpoint = null;
1322
+ this.browserbaseSessionId = null;
1323
+ this.browserbaseApiKey = null;
1324
+ this.browserUseSessionId = null;
1325
+ this.browserUseApiKey = null;
927
1326
  this.isPersistentContext = false;
928
1327
  this.activePageIndex = 0;
929
1328
  this.refMap = {};