agent-browser 0.17.1 → 0.19.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.
- package/README.md +194 -62
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser-darwin-x64 +0 -0
- package/bin/agent-browser-linux-arm64 +0 -0
- package/bin/agent-browser-linux-x64 +0 -0
- package/bin/agent-browser-win32-x64.exe +0 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +108 -45
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +50 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +245 -38
- package/dist/browser.js.map +1 -1
- package/dist/daemon.d.ts +5 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +62 -3
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-server.d.ts +26 -0
- package/dist/inspect-server.d.ts.map +1 -0
- package/dist/inspect-server.js +218 -0
- package/dist/inspect-server.js.map +1 -0
- package/dist/protocol.d.ts +3 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -3
- package/dist/protocol.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/skills/agent-browser/SKILL.md +116 -14
- package/skills/agent-browser/references/authentication.md +101 -0
- package/skills/agent-browser/references/commands.md +2 -0
- package/skills/vercel-sandbox/SKILL.md +280 -0
package/dist/browser.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getEncryptionKey, isEncryptedPayload, decryptData, ENCRYPTION_KEY_ENV,
|
|
|
12
12
|
* Can be overridden via the AGENT_BROWSER_DEFAULT_TIMEOUT environment variable.
|
|
13
13
|
* Default is 25s, which is below the CLI's 30s IPC read timeout to ensure
|
|
14
14
|
* Playwright errors are returned before the CLI gives up with EAGAIN.
|
|
15
|
-
*
|
|
15
|
+
* Recording contexts use a shorter fixed timeout (10s) and are not affected.
|
|
16
16
|
*/
|
|
17
17
|
export function getDefaultTimeout() {
|
|
18
18
|
const envValue = process.env.AGENT_BROWSER_DEFAULT_TIMEOUT;
|
|
@@ -24,12 +24,32 @@ export function getDefaultTimeout() {
|
|
|
24
24
|
}
|
|
25
25
|
return 25000;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Handles boolean env vars and parsing (e.g., "true", "1", "false", "0"),
|
|
29
|
+
* with a default value if not set or invalid
|
|
30
|
+
*/
|
|
31
|
+
export function parseBooleanEnvVar(name, defaultValue) {
|
|
32
|
+
const truthyVals = ['1', 'true'];
|
|
33
|
+
const falsyVals = ['0', 'false'];
|
|
34
|
+
if (!Object.hasOwn(process.env, name)) {
|
|
35
|
+
return defaultValue;
|
|
36
|
+
}
|
|
37
|
+
const param = process.env[name].toLowerCase();
|
|
38
|
+
if (truthyVals.includes(param)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (falsyVals.includes(param)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return defaultValue;
|
|
45
|
+
}
|
|
27
46
|
/**
|
|
28
47
|
* Manages the Playwright browser lifecycle with multiple tabs/windows
|
|
29
48
|
*/
|
|
30
49
|
export class BrowserManager {
|
|
31
50
|
browser = null;
|
|
32
51
|
cdpEndpoint = null; // stores port number or full URL
|
|
52
|
+
resolvedWsUrl = null;
|
|
33
53
|
isPersistentContext = false;
|
|
34
54
|
browserbaseSessionId = null;
|
|
35
55
|
browserbaseApiKey = null;
|
|
@@ -37,6 +57,7 @@ export class BrowserManager {
|
|
|
37
57
|
browserUseApiKey = null;
|
|
38
58
|
kernelSessionId = null;
|
|
39
59
|
kernelApiKey = null;
|
|
60
|
+
browserlessStopUrl = null;
|
|
40
61
|
contexts = [];
|
|
41
62
|
pages = [];
|
|
42
63
|
activePageIndex = 0;
|
|
@@ -53,6 +74,17 @@ export class BrowserManager {
|
|
|
53
74
|
colorScheme = null;
|
|
54
75
|
downloadPath = null;
|
|
55
76
|
allowedDomains = [];
|
|
77
|
+
inspectServer = null;
|
|
78
|
+
stopInspectServer() {
|
|
79
|
+
if (this.inspectServer) {
|
|
80
|
+
this.inspectServer.stop();
|
|
81
|
+
this.inspectServer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
setInspectServer(server) {
|
|
85
|
+
this.stopInspectServer();
|
|
86
|
+
this.inspectServer = server;
|
|
87
|
+
}
|
|
56
88
|
/**
|
|
57
89
|
* Set the persistent color scheme preference.
|
|
58
90
|
* Applied automatically to all new pages and contexts.
|
|
@@ -94,6 +126,19 @@ export class BrowserManager {
|
|
|
94
126
|
isLaunched() {
|
|
95
127
|
return this.browser !== null || this.isPersistentContext;
|
|
96
128
|
}
|
|
129
|
+
getCdpUrl() {
|
|
130
|
+
if (this.resolvedWsUrl)
|
|
131
|
+
return this.resolvedWsUrl;
|
|
132
|
+
if (this.cdpEndpoint?.startsWith('ws://') || this.cdpEndpoint?.startsWith('wss://')) {
|
|
133
|
+
return this.cdpEndpoint;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return this.browser?.wsEndpoint?.() ?? null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
97
142
|
/**
|
|
98
143
|
* Get enhanced snapshot with refs and cache the ref map
|
|
99
144
|
*/
|
|
@@ -312,6 +357,40 @@ export class BrowserManager {
|
|
|
312
357
|
this.activeFrame = frame;
|
|
313
358
|
}
|
|
314
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Navigate the active page to a URL and return the resolved URL + title.
|
|
362
|
+
* If the browser is launched but all pages have been closed, a new page is
|
|
363
|
+
* created automatically before navigating (stale-session recovery).
|
|
364
|
+
*/
|
|
365
|
+
async navigate(url, options = {}) {
|
|
366
|
+
this.checkDomainAllowed(url);
|
|
367
|
+
await this.ensurePage();
|
|
368
|
+
if (options.headers && Object.keys(options.headers).length > 0) {
|
|
369
|
+
await this.setScopedHeaders(url, options.headers);
|
|
370
|
+
}
|
|
371
|
+
const page = this.getPage();
|
|
372
|
+
await page.goto(url, {
|
|
373
|
+
waitUntil: options.waitUntil ?? 'load',
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
url: page.url(),
|
|
377
|
+
title: await page.title(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get the active page URL.
|
|
382
|
+
*/
|
|
383
|
+
async getUrl() {
|
|
384
|
+
await this.ensurePage();
|
|
385
|
+
return this.getPage().url();
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get the active page title.
|
|
389
|
+
*/
|
|
390
|
+
async getTitle() {
|
|
391
|
+
await this.ensurePage();
|
|
392
|
+
return this.getPage().title();
|
|
393
|
+
}
|
|
315
394
|
/**
|
|
316
395
|
* Switch back to main frame
|
|
317
396
|
*/
|
|
@@ -719,12 +798,17 @@ export class BrowserManager {
|
|
|
719
798
|
* Close a Browserbase session via API
|
|
720
799
|
*/
|
|
721
800
|
async closeBrowserbaseSession(sessionId, apiKey) {
|
|
722
|
-
await fetch(`https://api.browserbase.com/v1/sessions/${sessionId}`, {
|
|
723
|
-
method: '
|
|
801
|
+
const response = await fetch(`https://api.browserbase.com/v1/sessions/${sessionId}`, {
|
|
802
|
+
method: 'POST',
|
|
724
803
|
headers: {
|
|
804
|
+
'Content-Type': 'application/json',
|
|
725
805
|
'X-BB-API-Key': apiKey,
|
|
726
806
|
},
|
|
807
|
+
body: JSON.stringify({ status: 'REQUEST_RELEASE' }),
|
|
727
808
|
});
|
|
809
|
+
if (!response.ok) {
|
|
810
|
+
throw new Error(`Failed to close Browserbase session: ${response.statusText}`);
|
|
811
|
+
}
|
|
728
812
|
}
|
|
729
813
|
/**
|
|
730
814
|
* Close a Browser Use session via API
|
|
@@ -746,35 +830,43 @@ export class BrowserManager {
|
|
|
746
830
|
* Close a Kernel session via API
|
|
747
831
|
*/
|
|
748
832
|
async closeKernelSession(sessionId, apiKey) {
|
|
833
|
+
const headers = {};
|
|
834
|
+
if (apiKey) {
|
|
835
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
836
|
+
}
|
|
749
837
|
const response = await fetch(`https://api.onkernel.com/browsers/${sessionId}`, {
|
|
750
838
|
method: 'DELETE',
|
|
751
|
-
headers
|
|
752
|
-
Authorization: `Bearer ${apiKey}`,
|
|
753
|
-
},
|
|
839
|
+
headers,
|
|
754
840
|
});
|
|
755
841
|
if (!response.ok) {
|
|
756
842
|
throw new Error(`Failed to close Kernel session: ${response.statusText}`);
|
|
757
843
|
}
|
|
758
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Close a Browserless session via its stop URL
|
|
847
|
+
*/
|
|
848
|
+
async closeBrowserlessSession(stopUrl) {
|
|
849
|
+
const response = await fetch(stopUrl, {
|
|
850
|
+
method: 'DELETE',
|
|
851
|
+
});
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
throw new Error(`Failed to close Browserless session: ${response.statusText}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
759
856
|
/**
|
|
760
857
|
* Connect to Browserbase remote browser via CDP.
|
|
761
|
-
* Requires BROWSERBASE_API_KEY
|
|
858
|
+
* Requires BROWSERBASE_API_KEY environment variable.
|
|
762
859
|
*/
|
|
763
860
|
async connectToBrowserbase() {
|
|
764
861
|
const browserbaseApiKey = process.env.BROWSERBASE_API_KEY;
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
throw new Error('BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are required when using browserbase as a provider');
|
|
862
|
+
if (!browserbaseApiKey) {
|
|
863
|
+
throw new Error('BROWSERBASE_API_KEY is required when using browserbase as a provider');
|
|
768
864
|
}
|
|
769
865
|
const response = await fetch('https://api.browserbase.com/v1/sessions', {
|
|
770
866
|
method: 'POST',
|
|
771
867
|
headers: {
|
|
772
|
-
'Content-Type': 'application/json',
|
|
773
868
|
'X-BB-API-Key': browserbaseApiKey,
|
|
774
869
|
},
|
|
775
|
-
body: JSON.stringify({
|
|
776
|
-
projectId: browserbaseProjectId,
|
|
777
|
-
}),
|
|
778
870
|
});
|
|
779
871
|
if (!response.ok) {
|
|
780
872
|
throw new Error(`Failed to create Browserbase session: ${response.statusText}`);
|
|
@@ -794,7 +886,7 @@ export class BrowserManager {
|
|
|
794
886
|
this.browserbaseSessionId = session.id;
|
|
795
887
|
this.browserbaseApiKey = browserbaseApiKey;
|
|
796
888
|
this.browser = browser;
|
|
797
|
-
context.setDefaultTimeout(
|
|
889
|
+
context.setDefaultTimeout(getDefaultTimeout());
|
|
798
890
|
this.contexts.push(context);
|
|
799
891
|
this.setupContextTracking(context);
|
|
800
892
|
await this.ensureDomainFilter(context);
|
|
@@ -815,12 +907,14 @@ export class BrowserManager {
|
|
|
815
907
|
* Returns the profile object if successful.
|
|
816
908
|
*/
|
|
817
909
|
async findOrCreateKernelProfile(profileName, apiKey) {
|
|
910
|
+
const headers = {};
|
|
911
|
+
if (apiKey) {
|
|
912
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
913
|
+
}
|
|
818
914
|
// First, try to get the existing profile
|
|
819
915
|
const getResponse = await fetch(`https://api.onkernel.com/profiles/${encodeURIComponent(profileName)}`, {
|
|
820
916
|
method: 'GET',
|
|
821
|
-
headers
|
|
822
|
-
Authorization: `Bearer ${apiKey}`,
|
|
823
|
-
},
|
|
917
|
+
headers,
|
|
824
918
|
});
|
|
825
919
|
if (getResponse.ok) {
|
|
826
920
|
// Profile exists, return it
|
|
@@ -834,7 +928,7 @@ export class BrowserManager {
|
|
|
834
928
|
method: 'POST',
|
|
835
929
|
headers: {
|
|
836
930
|
'Content-Type': 'application/json',
|
|
837
|
-
|
|
931
|
+
...headers,
|
|
838
932
|
},
|
|
839
933
|
body: JSON.stringify({ name: profileName }),
|
|
840
934
|
});
|
|
@@ -845,13 +939,13 @@ export class BrowserManager {
|
|
|
845
939
|
}
|
|
846
940
|
/**
|
|
847
941
|
* Connect to Kernel remote browser via CDP.
|
|
848
|
-
*
|
|
942
|
+
* Uses KERNEL_API_KEY environment variable for authentication when set.
|
|
943
|
+
* When running inside environments with external credential injection
|
|
944
|
+
* (e.g. Vercel Sandbox credentials brokering), the API key can be omitted
|
|
945
|
+
* and auth headers will be injected at the network layer.
|
|
849
946
|
*/
|
|
850
947
|
async connectToKernel() {
|
|
851
948
|
const kernelApiKey = process.env.KERNEL_API_KEY;
|
|
852
|
-
if (!kernelApiKey) {
|
|
853
|
-
throw new Error('KERNEL_API_KEY is required when using kernel as a provider');
|
|
854
|
-
}
|
|
855
949
|
// Find or create profile if KERNEL_PROFILE_NAME is set
|
|
856
950
|
const profileName = process.env.KERNEL_PROFILE_NAME;
|
|
857
951
|
let profileConfig;
|
|
@@ -864,12 +958,15 @@ export class BrowserManager {
|
|
|
864
958
|
},
|
|
865
959
|
};
|
|
866
960
|
}
|
|
961
|
+
const headers = {
|
|
962
|
+
'Content-Type': 'application/json',
|
|
963
|
+
};
|
|
964
|
+
if (kernelApiKey) {
|
|
965
|
+
headers['Authorization'] = `Bearer ${kernelApiKey}`;
|
|
966
|
+
}
|
|
867
967
|
const response = await fetch('https://api.onkernel.com/browsers', {
|
|
868
968
|
method: 'POST',
|
|
869
|
-
headers
|
|
870
|
-
'Content-Type': 'application/json',
|
|
871
|
-
Authorization: `Bearer ${kernelApiKey}`,
|
|
872
|
-
},
|
|
969
|
+
headers,
|
|
873
970
|
body: JSON.stringify({
|
|
874
971
|
// Kernel browsers are headful by default with stealth mode available
|
|
875
972
|
// The user can configure these via environment variables if needed
|
|
@@ -911,7 +1008,7 @@ export class BrowserManager {
|
|
|
911
1008
|
page = pages[0] ?? (await context.newPage());
|
|
912
1009
|
}
|
|
913
1010
|
this.kernelSessionId = session.session_id;
|
|
914
|
-
this.kernelApiKey = kernelApiKey;
|
|
1011
|
+
this.kernelApiKey = kernelApiKey ?? null;
|
|
915
1012
|
this.browser = browser;
|
|
916
1013
|
context.setDefaultTimeout(getDefaultTimeout());
|
|
917
1014
|
this.contexts.push(context);
|
|
@@ -994,6 +1091,82 @@ export class BrowserManager {
|
|
|
994
1091
|
throw error;
|
|
995
1092
|
}
|
|
996
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Connect to Browserless remote browser via CDP.
|
|
1096
|
+
* Requires BROWSERLESS_API_KEY environment variable.
|
|
1097
|
+
*/
|
|
1098
|
+
async connectToBrowserless() {
|
|
1099
|
+
const browserlessToken = process.env.BROWSERLESS_API_KEY;
|
|
1100
|
+
if (!browserlessToken) {
|
|
1101
|
+
throw new Error('BROWSERLESS_API_KEY is required when using browserless as a provider');
|
|
1102
|
+
}
|
|
1103
|
+
const supportedBrowsers = ['chromium', 'chrome'];
|
|
1104
|
+
const apiUrl = process.env.BROWSERLESS_API_URL || 'https://production-sfo.browserless.io';
|
|
1105
|
+
const browserType = process.env.BROWSERLESS_BROWSER_TYPE || 'chromium';
|
|
1106
|
+
const ttl = parseInt(process.env.BROWSERLESS_TTL || '300000', 10);
|
|
1107
|
+
const stealth = parseBooleanEnvVar('BROWSERLESS_STEALTH', true);
|
|
1108
|
+
if (!supportedBrowsers.includes(browserType)) {
|
|
1109
|
+
throw new Error(`BROWSERLESS_BROWSER_TYPE "${browserType}" is not supported. Only ${supportedBrowsers.join(', ')} are allowed.`);
|
|
1110
|
+
}
|
|
1111
|
+
const response = await fetch(`${apiUrl}/session?token=${encodeURIComponent(browserlessToken)}`, {
|
|
1112
|
+
method: 'POST',
|
|
1113
|
+
headers: {
|
|
1114
|
+
'Content-Type': 'application/json',
|
|
1115
|
+
},
|
|
1116
|
+
body: JSON.stringify({
|
|
1117
|
+
ttl,
|
|
1118
|
+
stealth,
|
|
1119
|
+
browser: browserType,
|
|
1120
|
+
}),
|
|
1121
|
+
});
|
|
1122
|
+
if (!response.ok) {
|
|
1123
|
+
throw new Error(`Failed to create Browserless session: ${response.statusText}`);
|
|
1124
|
+
}
|
|
1125
|
+
let session;
|
|
1126
|
+
try {
|
|
1127
|
+
session = (await response.json());
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
throw new Error(`Failed to parse Browserless session response: ${error instanceof Error ? error.message : String(error)}`);
|
|
1131
|
+
}
|
|
1132
|
+
if (!session.connect || !session.stop) {
|
|
1133
|
+
throw new Error(`Invalid Browserless session response: missing ${!session.connect ? 'connect' : 'stop'}`);
|
|
1134
|
+
}
|
|
1135
|
+
const browser = await chromium.connectOverCDP(session.connect).catch(() => {
|
|
1136
|
+
throw new Error('Failed to connect to Browserless session via CDP');
|
|
1137
|
+
});
|
|
1138
|
+
try {
|
|
1139
|
+
const contexts = browser.contexts();
|
|
1140
|
+
let context;
|
|
1141
|
+
let page;
|
|
1142
|
+
if (contexts.length === 0) {
|
|
1143
|
+
context = await browser.newContext();
|
|
1144
|
+
page = await context.newPage();
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
context = contexts[0];
|
|
1148
|
+
const pages = context.pages();
|
|
1149
|
+
page = pages[0] ?? (await context.newPage());
|
|
1150
|
+
}
|
|
1151
|
+
this.browser = browser;
|
|
1152
|
+
this.browserlessStopUrl = session.stop;
|
|
1153
|
+
context.setDefaultTimeout(getDefaultTimeout());
|
|
1154
|
+
this.contexts.push(context);
|
|
1155
|
+
this.setupContextTracking(context);
|
|
1156
|
+
await this.ensureDomainFilter(context);
|
|
1157
|
+
await this.sanitizeExistingPages([page]);
|
|
1158
|
+
this.pages.push(page);
|
|
1159
|
+
this.activePageIndex = 0;
|
|
1160
|
+
this.setupPageTracking(page);
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
await this.closeBrowserlessSession(session.stop).catch((sessionError) => {
|
|
1164
|
+
console.error('Failed to close Browserless session during cleanup:', sessionError);
|
|
1165
|
+
});
|
|
1166
|
+
this.browserlessStopUrl = null;
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
997
1170
|
/**
|
|
998
1171
|
* Launch the browser with the specified options
|
|
999
1172
|
* If already launched, this is a no-op (browser stays open)
|
|
@@ -1080,6 +1253,10 @@ export class BrowserManager {
|
|
|
1080
1253
|
await this.connectToKernel();
|
|
1081
1254
|
return;
|
|
1082
1255
|
}
|
|
1256
|
+
if (provider === 'browserless') {
|
|
1257
|
+
await this.connectToBrowserless();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1083
1260
|
if (this.downloadPath) {
|
|
1084
1261
|
const resolved = path.resolve(this.downloadPath);
|
|
1085
1262
|
const stat = statSync(resolved, { throwIfNoEntry: false });
|
|
@@ -1174,6 +1351,7 @@ export class BrowserManager {
|
|
|
1174
1351
|
...(this.downloadPath && { downloadsPath: this.downloadPath }),
|
|
1175
1352
|
});
|
|
1176
1353
|
this.cdpEndpoint = null;
|
|
1354
|
+
this.resolvedWsUrl = null;
|
|
1177
1355
|
// Check for auto-load state file (supports encrypted files)
|
|
1178
1356
|
let storageState = options.storageState ? options.storageState : undefined;
|
|
1179
1357
|
if (!storageState && options.autoStateFilePath) {
|
|
@@ -1265,17 +1443,17 @@ export class BrowserManager {
|
|
|
1265
1443
|
}
|
|
1266
1444
|
else if (/^\d+$/.test(cdpEndpoint)) {
|
|
1267
1445
|
// Numeric string - treat as port number (handles JSON serialization quirks)
|
|
1268
|
-
cdpUrl = `http://
|
|
1446
|
+
cdpUrl = `http://127.0.0.1:${cdpEndpoint}`;
|
|
1269
1447
|
}
|
|
1270
1448
|
else {
|
|
1271
1449
|
// Unknown format - still try as port for backward compatibility
|
|
1272
|
-
cdpUrl = `http://
|
|
1450
|
+
cdpUrl = `http://127.0.0.1:${cdpEndpoint}`;
|
|
1273
1451
|
}
|
|
1274
1452
|
const browser = await chromium
|
|
1275
1453
|
.connectOverCDP(cdpUrl, { timeout: options?.timeout })
|
|
1276
1454
|
.catch(() => {
|
|
1277
1455
|
throw new Error(`Failed to connect via CDP to ${cdpUrl}. ` +
|
|
1278
|
-
(cdpUrl.includes('
|
|
1456
|
+
(cdpUrl.includes('127.0.0.1')
|
|
1279
1457
|
? `Make sure the app is running with --remote-debugging-port=${cdpEndpoint}`
|
|
1280
1458
|
: 'Make sure the remote browser is accessible and the URL is correct.'));
|
|
1281
1459
|
});
|
|
@@ -1293,8 +1471,26 @@ export class BrowserManager {
|
|
|
1293
1471
|
// All validation passed - commit state
|
|
1294
1472
|
this.browser = browser;
|
|
1295
1473
|
this.cdpEndpoint = cdpEndpoint;
|
|
1474
|
+
let resolvedWs = null;
|
|
1475
|
+
try {
|
|
1476
|
+
resolvedWs = browser.wsEndpoint?.() ?? null;
|
|
1477
|
+
}
|
|
1478
|
+
catch (err) {
|
|
1479
|
+
console.error('[inspect] wsEndpoint() failed:', err);
|
|
1480
|
+
}
|
|
1481
|
+
if (!resolvedWs && (cdpUrl.startsWith('http://') || cdpUrl.startsWith('https://'))) {
|
|
1482
|
+
try {
|
|
1483
|
+
const resp = await fetch(`${cdpUrl}/json/version`);
|
|
1484
|
+
const info = await resp.json();
|
|
1485
|
+
resolvedWs = info.webSocketDebuggerUrl ?? null;
|
|
1486
|
+
}
|
|
1487
|
+
catch (err) {
|
|
1488
|
+
console.error('[inspect] /json/version fetch failed:', err);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
this.resolvedWsUrl = resolvedWs;
|
|
1296
1492
|
for (const context of contexts) {
|
|
1297
|
-
context.setDefaultTimeout(
|
|
1493
|
+
context.setDefaultTimeout(getDefaultTimeout());
|
|
1298
1494
|
this.contexts.push(context);
|
|
1299
1495
|
this.setupContextTracking(context);
|
|
1300
1496
|
await this.ensureDomainFilter(context);
|
|
@@ -1510,7 +1706,7 @@ export class BrowserManager {
|
|
|
1510
1706
|
* Create a new tab in the current context
|
|
1511
1707
|
*/
|
|
1512
1708
|
async newTab() {
|
|
1513
|
-
if (!this.
|
|
1709
|
+
if (!this.isLaunched() || this.contexts.length === 0) {
|
|
1514
1710
|
throw new Error('Browser not launched');
|
|
1515
1711
|
}
|
|
1516
1712
|
// Invalidate CDP session since we're switching to a new page
|
|
@@ -1530,7 +1726,9 @@ export class BrowserManager {
|
|
|
1530
1726
|
*/
|
|
1531
1727
|
async newWindow(viewport) {
|
|
1532
1728
|
if (!this.browser) {
|
|
1533
|
-
throw new Error(
|
|
1729
|
+
throw new Error(this.isPersistentContext
|
|
1730
|
+
? 'newWindow is not supported in extension (persistent context) mode'
|
|
1731
|
+
: 'Browser not launched');
|
|
1534
1732
|
}
|
|
1535
1733
|
const context = await this.browser.newContext({
|
|
1536
1734
|
viewport: viewport === undefined ? { width: 1280, height: 720 } : viewport,
|
|
@@ -1931,8 +2129,8 @@ export class BrowserManager {
|
|
|
1931
2129
|
this.recordingTempDir = path.join(os.tmpdir(), `agent-browser-recording-${session}-${Date.now()}`);
|
|
1932
2130
|
mkdirSync(this.recordingTempDir, { recursive: true });
|
|
1933
2131
|
this.recordingOutputPath = outputPath;
|
|
1934
|
-
//
|
|
1935
|
-
const viewport = { width: 1280, height: 720 };
|
|
2132
|
+
// Reuse the active page viewport when available so recording matches the current layout.
|
|
2133
|
+
const viewport = currentPage?.viewportSize() ?? { width: 1280, height: 720 };
|
|
1936
2134
|
this.recordingContext = await this.browser.newContext({
|
|
1937
2135
|
viewport,
|
|
1938
2136
|
recordVideo: {
|
|
@@ -2045,6 +2243,7 @@ export class BrowserManager {
|
|
|
2045
2243
|
* Close the browser and clean up
|
|
2046
2244
|
*/
|
|
2047
2245
|
async close() {
|
|
2246
|
+
this.stopInspectServer();
|
|
2048
2247
|
// Stop recording if active (saves video)
|
|
2049
2248
|
if (this.recordingContext) {
|
|
2050
2249
|
await this.stopRecording();
|
|
@@ -2089,12 +2288,18 @@ export class BrowserManager {
|
|
|
2089
2288
|
});
|
|
2090
2289
|
this.browser = null;
|
|
2091
2290
|
}
|
|
2092
|
-
else if (this.kernelSessionId
|
|
2093
|
-
await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey).catch((error) => {
|
|
2291
|
+
else if (this.kernelSessionId) {
|
|
2292
|
+
await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey ?? undefined).catch((error) => {
|
|
2094
2293
|
console.error('Failed to close Kernel session:', error);
|
|
2095
2294
|
});
|
|
2096
2295
|
this.browser = null;
|
|
2097
2296
|
}
|
|
2297
|
+
else if (this.browserlessStopUrl) {
|
|
2298
|
+
await this.closeBrowserlessSession(this.browserlessStopUrl).catch((error) => {
|
|
2299
|
+
console.error('Failed to close Browserless session:', error);
|
|
2300
|
+
});
|
|
2301
|
+
this.browser = null;
|
|
2302
|
+
}
|
|
2098
2303
|
else if (this.cdpEndpoint !== null) {
|
|
2099
2304
|
// CDP: only disconnect, don't close external app's pages
|
|
2100
2305
|
if (this.browser) {
|
|
@@ -2118,12 +2323,14 @@ export class BrowserManager {
|
|
|
2118
2323
|
this.pages = [];
|
|
2119
2324
|
this.contexts = [];
|
|
2120
2325
|
this.cdpEndpoint = null;
|
|
2326
|
+
this.resolvedWsUrl = null;
|
|
2121
2327
|
this.browserbaseSessionId = null;
|
|
2122
2328
|
this.browserbaseApiKey = null;
|
|
2123
2329
|
this.browserUseSessionId = null;
|
|
2124
2330
|
this.browserUseApiKey = null;
|
|
2125
2331
|
this.kernelSessionId = null;
|
|
2126
2332
|
this.kernelApiKey = null;
|
|
2333
|
+
this.browserlessStopUrl = null;
|
|
2127
2334
|
this.isPersistentContext = false;
|
|
2128
2335
|
this.activePageIndex = 0;
|
|
2129
2336
|
this.colorScheme = null;
|