@browserbasehq/browse-cli 0.3.0 → 0.4.1-alpha-3917df4
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/LICENSE +21 -0
- package/README.md +52 -6
- package/dist/index.js +418 -98
- package/package.json +16 -16
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Browserbase Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -173,11 +173,47 @@ browse env
|
|
|
173
173
|
# Switch current session to Browserbase (restarts daemon if needed)
|
|
174
174
|
browse env remote
|
|
175
175
|
|
|
176
|
-
# Switch back to local Chrome
|
|
176
|
+
# Switch back to local Chrome (auto-discovers existing Chrome, falls back to isolated)
|
|
177
177
|
browse env local
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
#### Local Browser Strategies
|
|
181
|
+
|
|
182
|
+
By default, `browse env local` auto-discovers an already-running Chrome with remote
|
|
183
|
+
debugging enabled. This lets agents use your existing cookies, logins, and browser state.
|
|
184
|
+
If no debuggable Chrome is found, it falls back to launching an isolated browser.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Auto-discover local Chrome, fallback to isolated (default)
|
|
188
|
+
browse env local
|
|
189
|
+
|
|
190
|
+
# Force a clean isolated browser (no auto-discovery)
|
|
191
|
+
browse env local --isolated
|
|
192
|
+
|
|
193
|
+
# Attach to a specific CDP target (port or URL)
|
|
194
|
+
browse env local 9222
|
|
195
|
+
browse env local ws://localhost:9222/devtools/browser/...
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Auto-discovery checks:
|
|
199
|
+
1. `DevToolsActivePort` files in well-known Chrome/Chromium/Brave user-data directories
|
|
200
|
+
2. Common debugging ports (9222, 9229)
|
|
201
|
+
|
|
202
|
+
To make your Chrome discoverable:
|
|
203
|
+
|
|
204
|
+
1. Open `chrome://inspect/#remote-debugging`
|
|
205
|
+
2. Check the box **"Allow remote debugging for this browser instance"**
|
|
206
|
+
|
|
207
|
+
For more information, see the [Chrome DevTools docs](https://developer.chrome.com/blog/chrome-devtools-mcp-debug-your-browser-session).
|
|
208
|
+
|
|
209
|
+
Use `browse status` to see which strategy was resolved:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
browse status
|
|
213
|
+
# {"running":true,"session":"default","mode":"local","localStrategy":"auto","localSource":"attached-existing","resolvedCdpUrl":"ws://..."}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### General Behavior
|
|
181
217
|
|
|
182
218
|
- Environment is scoped per `--session`
|
|
183
219
|
- `browse env <target>` persists an override and restarts the daemon
|
|
@@ -193,7 +229,7 @@ Behavior details:
|
|
|
193
229
|
| `--session <name>` | Session name for multiple browsers (default: "default") |
|
|
194
230
|
| `--headless` | Run Chrome in headless mode |
|
|
195
231
|
| `--headed` | Run Chrome with visible window (default) |
|
|
196
|
-
| `--ws <url>` |
|
|
232
|
+
| `--ws <url\|port>` | One-shot CDP connection (bypasses daemon) |
|
|
197
233
|
| `--json` | Output as JSON |
|
|
198
234
|
|
|
199
235
|
## Environment Variables
|
|
@@ -249,11 +285,21 @@ browse --session personal open https://twitter.com
|
|
|
249
285
|
|
|
250
286
|
Connect to an existing Chrome instance:
|
|
251
287
|
|
|
288
|
+
To make your Chrome discoverable:
|
|
289
|
+
|
|
290
|
+
1. Open `chrome://inspect/#remote-debugging`
|
|
291
|
+
2. Check the box **"Allow remote debugging for this browser instance"**
|
|
292
|
+
3. Re-run the CLI and it will auto-connect!
|
|
293
|
+
|
|
294
|
+
For more information, see the [Chrome DevTools docs](https://developer.chrome.com/blog/chrome-devtools-mcp-debug-your-browser-session).
|
|
295
|
+
|
|
252
296
|
```bash
|
|
253
|
-
#
|
|
254
|
-
|
|
297
|
+
# Auto-discover Chrome with remote debugging enabled
|
|
298
|
+
browse env local
|
|
299
|
+
browse open https://example.com
|
|
255
300
|
|
|
256
|
-
#
|
|
301
|
+
# Or target a specific port / WebSocket URL
|
|
302
|
+
browse env local 9222
|
|
257
303
|
browse --ws ws://localhost:9222/devtools/browser/... open https://example.com
|
|
258
304
|
```
|
|
259
305
|
|
package/dist/index.js
CHANGED
|
@@ -34,9 +34,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
34
34
|
));
|
|
35
35
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
36
|
|
|
37
|
-
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@
|
|
37
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.19.4_typescript@5.8.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
|
|
38
38
|
var init_cjs_shims = __esm({
|
|
39
|
-
"../../node_modules/.pnpm/tsup@8.5.1_jiti@
|
|
39
|
+
"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.19.4_typescript@5.8.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js"() {
|
|
40
40
|
"use strict";
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -94180,7 +94180,7 @@ var init_page2 = __esm({
|
|
|
94180
94180
|
});
|
|
94181
94181
|
try {
|
|
94182
94182
|
if (this.apiClient) {
|
|
94183
|
-
const result = await this.apiClient.goto(url2, { waitUntil: options?.waitUntil }, this.mainFrameId());
|
|
94183
|
+
const result = await this.apiClient.goto(url2, { waitUntil: options?.waitUntil, timeout: options?.timeoutMs }, this.mainFrameId());
|
|
94184
94184
|
this._currentUrl = url2;
|
|
94185
94185
|
if (isSerializableResponse(result)) {
|
|
94186
94186
|
return Response2.fromSerializable(result, {
|
|
@@ -100088,6 +100088,18 @@ Return the element that matches the instruction if it exists. Otherwise, return
|
|
|
100088
100088
|
content: [content, buildUserInstructionsString(userProvidedInstructions)].filter(Boolean).join("\n\n")
|
|
100089
100089
|
};
|
|
100090
100090
|
}
|
|
100091
|
+
function buildActVariablesPrompt(variables) {
|
|
100092
|
+
if (!variables || Object.keys(variables).length === 0) {
|
|
100093
|
+
return "";
|
|
100094
|
+
}
|
|
100095
|
+
const variableNames = Object.keys(variables).map((key) => `%${key}%`).join(", ");
|
|
100096
|
+
return ` The user has provided the following variables to be used in the action: ${variableNames}
|
|
100097
|
+
|
|
100098
|
+
Note that these are the variable names/keys, and not the actual variable values.
|
|
100099
|
+
|
|
100100
|
+
To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken.
|
|
100101
|
+
`;
|
|
100102
|
+
}
|
|
100091
100103
|
function buildActPrompt(action, supportedActions, variables) {
|
|
100092
100104
|
let instruction = `Find the most relevant element to perform an action on given the following action: ${action}.
|
|
100093
100105
|
IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.
|
|
@@ -100114,11 +100126,7 @@ function buildActPrompt(action, supportedActions, variables) {
|
|
|
100114
100126
|
- choose the 'click' method
|
|
100115
100127
|
- set twoStep to true.
|
|
100116
100128
|
`;
|
|
100117
|
-
|
|
100118
|
-
const variableNames = Object.keys(variables).map((key) => `%${key}%`).join(", ");
|
|
100119
|
-
const variablesPrompt = `The following variables are available to use in the action: ${variableNames}. Fill the argument variables with the variable name.`;
|
|
100120
|
-
instruction += ` ${variablesPrompt}`;
|
|
100121
|
-
}
|
|
100129
|
+
instruction += buildActVariablesPrompt(variables);
|
|
100122
100130
|
return instruction;
|
|
100123
100131
|
}
|
|
100124
100132
|
function buildStepTwoPrompt(originalUserAction, previousAction, supportedActions, variables) {
|
|
@@ -100136,11 +100144,7 @@ function buildStepTwoPrompt(originalUserAction, previousAction, supportedActions
|
|
|
100136
100144
|
If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.
|
|
100137
100145
|
If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument \u2014 e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys.
|
|
100138
100146
|
`;
|
|
100139
|
-
|
|
100140
|
-
const variableNames = Object.keys(variables).map((key) => `%${key}%`).join(", ");
|
|
100141
|
-
const variablesPrompt = `The following variables are available to use in the action: ${variableNames}. Fill the argument variables with the variable name.`;
|
|
100142
|
-
instruction += ` ${variablesPrompt}`;
|
|
100143
|
-
}
|
|
100147
|
+
instruction += buildActVariablesPrompt(variables);
|
|
100144
100148
|
return instruction;
|
|
100145
100149
|
}
|
|
100146
100150
|
function buildGoogleCUASystemPrompt() {
|
|
@@ -100448,7 +100452,7 @@ async function observe({ instruction, domElements, llmClient, userProvidedInstru
|
|
|
100448
100452
|
async function act({ instruction, domElements, llmClient, userProvidedInstructions, logger, logInferenceToFile = false }) {
|
|
100449
100453
|
const isGPT5 = llmClient.modelName.includes("gpt-5");
|
|
100450
100454
|
const actSchema = external_exports.object({
|
|
100451
|
-
elementId: external_exports.string().regex(/^\d+-\d+$/).describe("the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'."),
|
|
100455
|
+
elementId: external_exports.string().regex(/^\d+-\d+$/).describe("the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'"),
|
|
100452
100456
|
description: external_exports.string().describe("a description of the accessible element and its purpose"),
|
|
100453
100457
|
method: external_exports.enum(
|
|
100454
100458
|
// Use Object.values() for Zod v3 compatibility - z.enum() in v3 doesn't accept TypeScript enums directly
|
|
@@ -119787,10 +119791,13 @@ var AISdkClient2 = class extends LLMClient {
|
|
|
119787
119791
|
type = "aisdk";
|
|
119788
119792
|
model;
|
|
119789
119793
|
logger;
|
|
119790
|
-
constructor({ model, logger }) {
|
|
119794
|
+
constructor({ model, logger, clientOptions }) {
|
|
119791
119795
|
super(model.modelId);
|
|
119792
119796
|
this.model = model;
|
|
119793
119797
|
this.logger = logger;
|
|
119798
|
+
if (clientOptions) {
|
|
119799
|
+
this.clientOptions = clientOptions;
|
|
119800
|
+
}
|
|
119794
119801
|
}
|
|
119795
119802
|
getLanguageModel() {
|
|
119796
119803
|
return this.model;
|
|
@@ -119868,9 +119875,11 @@ var AISdkClient2 = class extends LLMClient {
|
|
|
119868
119875
|
let objectResponse;
|
|
119869
119876
|
const isGPT5 = this.model.modelId.includes("gpt-5");
|
|
119870
119877
|
const isCodex = this.model.modelId.includes("codex");
|
|
119871
|
-
const usesLowReasoningEffort = (this.model.modelId.includes("gpt-5.1") || this.model.modelId.includes("gpt-5.2")) && !isCodex;
|
|
119872
119878
|
const isKimi = this.model.modelId.includes("kimi");
|
|
119873
119879
|
const temperature = isKimi ? 1 : options.temperature;
|
|
119880
|
+
const isGPT5SubModel = this.model.modelId.includes("gpt-5.") && !isCodex;
|
|
119881
|
+
const userReasoningEffort = this.clientOptions?.reasoningEffort;
|
|
119882
|
+
const resolvedReasoningEffort = userReasoningEffort ?? (isGPT5SubModel ? "none" : void 0);
|
|
119874
119883
|
const PROMPT_JSON_FALLBACK_PATTERNS = ["deepseek", "kimi", "glm"];
|
|
119875
119884
|
const needsPromptJsonFallback = PROMPT_JSON_FALLBACK_PATTERNS.some((p2) => this.model.modelId.includes(p2));
|
|
119876
119885
|
if (options.response_model) {
|
|
@@ -119899,11 +119908,10 @@ You must respond in JSON format. respond WITH JSON. Do not include any other tex
|
|
|
119899
119908
|
messages: formattedMessages,
|
|
119900
119909
|
schema: options.response_model.schema,
|
|
119901
119910
|
temperature,
|
|
119902
|
-
providerOptions:
|
|
119911
|
+
providerOptions: resolvedReasoningEffort ? {
|
|
119903
119912
|
openai: {
|
|
119904
|
-
textVerbosity: isCodex ? "medium" : "low",
|
|
119905
|
-
|
|
119906
|
-
reasoningEffort: isCodex ? "medium" : usesLowReasoningEffort ? "low" : "minimal"
|
|
119913
|
+
...isGPT5 ? { textVerbosity: isCodex ? "medium" : "low" } : {},
|
|
119914
|
+
reasoningEffort: resolvedReasoningEffort
|
|
119907
119915
|
}
|
|
119908
119916
|
} : void 0
|
|
119909
119917
|
});
|
|
@@ -152792,27 +152800,34 @@ var modelToProviderMap = {
|
|
|
152792
152800
|
"gemini-2.5-flash-preview-04-17": "google",
|
|
152793
152801
|
"gemini-2.5-pro-preview-03-25": "google"
|
|
152794
152802
|
};
|
|
152795
|
-
function getAISDKLanguageModel(subProvider, subModelName, clientOptions) {
|
|
152803
|
+
function getAISDKLanguageModel(subProvider, subModelName, clientOptions, middleware) {
|
|
152796
152804
|
const hasValidOptions = clientOptions && Object.values(clientOptions).some((v2) => v2 !== void 0 && v2 !== null);
|
|
152805
|
+
let model;
|
|
152797
152806
|
if (hasValidOptions) {
|
|
152798
152807
|
const creator = AISDKProvidersWithAPIKey[subProvider];
|
|
152799
152808
|
if (!creator) {
|
|
152800
152809
|
throw new UnsupportedAISDKModelProviderError(subProvider, Object.keys(AISDKProvidersWithAPIKey));
|
|
152801
152810
|
}
|
|
152802
152811
|
const provider = creator(clientOptions);
|
|
152803
|
-
|
|
152812
|
+
model = provider(subModelName);
|
|
152804
152813
|
} else {
|
|
152805
152814
|
const provider = AISDKProviders[subProvider];
|
|
152806
152815
|
if (!provider) {
|
|
152807
152816
|
throw new UnsupportedAISDKModelProviderError(subProvider, Object.keys(AISDKProviders));
|
|
152808
152817
|
}
|
|
152809
|
-
|
|
152818
|
+
model = provider(subModelName);
|
|
152810
152819
|
}
|
|
152820
|
+
if (middleware) {
|
|
152821
|
+
return wrapLanguageModel({ model, middleware });
|
|
152822
|
+
}
|
|
152823
|
+
return model;
|
|
152811
152824
|
}
|
|
152812
152825
|
var LLMProvider = class {
|
|
152813
152826
|
logger;
|
|
152814
|
-
|
|
152827
|
+
middleware;
|
|
152828
|
+
constructor(logger, middleware) {
|
|
152815
152829
|
this.logger = logger;
|
|
152830
|
+
this.middleware = middleware;
|
|
152816
152831
|
}
|
|
152817
152832
|
getClient(modelName, clientOptions, options) {
|
|
152818
152833
|
if (modelName.includes("/")) {
|
|
@@ -152822,10 +152837,12 @@ var LLMProvider = class {
|
|
|
152822
152837
|
if (subProvider === "vertex" && !options?.disableAPI && !options?.experimental) {
|
|
152823
152838
|
throw new ExperimentalNotConfiguredError("Vertex provider");
|
|
152824
152839
|
}
|
|
152825
|
-
const
|
|
152840
|
+
const effectiveMiddleware = options?.middleware ?? this.middleware;
|
|
152841
|
+
const languageModel = getAISDKLanguageModel(subProvider, subModelName, clientOptions, effectiveMiddleware);
|
|
152826
152842
|
return new AISdkClient2({
|
|
152827
152843
|
model: languageModel,
|
|
152828
|
-
logger: this.logger
|
|
152844
|
+
logger: this.logger,
|
|
152845
|
+
clientOptions
|
|
152829
152846
|
});
|
|
152830
152847
|
}
|
|
152831
152848
|
const provider = modelToProviderMap[modelName];
|
|
@@ -160612,7 +160629,9 @@ function hasInjectableDOM(url2) {
|
|
|
160612
160629
|
return false;
|
|
160613
160630
|
}
|
|
160614
160631
|
function isNonWebTarget(info) {
|
|
160615
|
-
|
|
160632
|
+
if (info.type === "page")
|
|
160633
|
+
return false;
|
|
160634
|
+
return info.type !== "iframe" || !hasInjectableDOM(info.url);
|
|
160616
160635
|
}
|
|
160617
160636
|
function isTopLevelPage(info) {
|
|
160618
160637
|
const ti = info;
|
|
@@ -162828,13 +162847,14 @@ function resolveModelConfiguration(model) {
|
|
|
162828
162847
|
return { modelName: model };
|
|
162829
162848
|
}
|
|
162830
162849
|
if (model && typeof model === "object") {
|
|
162831
|
-
const { modelName, ...clientOptions } = model;
|
|
162850
|
+
const { modelName, middleware, ...clientOptions } = model;
|
|
162832
162851
|
if (!modelName) {
|
|
162833
162852
|
throw new StagehandInvalidArgumentError("model.modelName is required when providing client options.");
|
|
162834
162853
|
}
|
|
162835
162854
|
return {
|
|
162836
162855
|
modelName,
|
|
162837
|
-
clientOptions
|
|
162856
|
+
clientOptions,
|
|
162857
|
+
middleware
|
|
162838
162858
|
};
|
|
162839
162859
|
}
|
|
162840
162860
|
return { modelName: DEFAULT_MODEL_NAME };
|
|
@@ -163009,11 +163029,11 @@ var V3 = (() => {
|
|
|
163009
163029
|
}
|
|
163010
163030
|
} catch {
|
|
163011
163031
|
}
|
|
163012
|
-
const { modelName, clientOptions } = resolveModelConfiguration(opts.model);
|
|
163032
|
+
const { modelName, clientOptions, middleware } = resolveModelConfiguration(opts.model);
|
|
163013
163033
|
this.modelName = modelName;
|
|
163014
163034
|
this.experimental = opts.experimental ?? false;
|
|
163015
163035
|
this.logInferenceToFile = opts.logInferenceToFile ?? false;
|
|
163016
|
-
this.llmProvider = new LLMProvider(this.logger);
|
|
163036
|
+
this.llmProvider = new LLMProvider(this.logger, middleware);
|
|
163017
163037
|
this.domSettleTimeoutMs = opts.domSettleTimeout;
|
|
163018
163038
|
this.disableAPI = opts.disableAPI ?? false;
|
|
163019
163039
|
const baseClientOptions = clientOptions ? { ...clientOptions } : {};
|
|
@@ -163095,14 +163115,16 @@ var V3 = (() => {
|
|
|
163095
163115
|
}
|
|
163096
163116
|
let modelName;
|
|
163097
163117
|
let clientOptions;
|
|
163118
|
+
let perCallMiddleware;
|
|
163098
163119
|
if (typeof model === "string") {
|
|
163099
163120
|
modelName = model;
|
|
163100
163121
|
} else {
|
|
163101
|
-
const { modelName: overrideModelName, ...rest } = model;
|
|
163122
|
+
const { modelName: overrideModelName, middleware, ...rest } = model;
|
|
163102
163123
|
modelName = overrideModelName;
|
|
163103
163124
|
clientOptions = rest;
|
|
163125
|
+
perCallMiddleware = middleware;
|
|
163104
163126
|
}
|
|
163105
|
-
if (modelName === this.modelName && (!clientOptions || Object.keys(clientOptions).length === 0)) {
|
|
163127
|
+
if (modelName === this.modelName && !perCallMiddleware && (!clientOptions || Object.keys(clientOptions).length === 0)) {
|
|
163106
163128
|
return this.llmClient;
|
|
163107
163129
|
}
|
|
163108
163130
|
const overrideProvider = String(modelName).split("/")[0];
|
|
@@ -163118,6 +163140,13 @@ var V3 = (() => {
|
|
|
163118
163140
|
mergedOptions.apiKey = apiKey;
|
|
163119
163141
|
}
|
|
163120
163142
|
}
|
|
163143
|
+
if (perCallMiddleware) {
|
|
163144
|
+
return this.llmProvider.getClient(modelName, mergedOptions, {
|
|
163145
|
+
experimental: this.experimental,
|
|
163146
|
+
disableAPI: this.disableAPI,
|
|
163147
|
+
middleware: perCallMiddleware
|
|
163148
|
+
});
|
|
163149
|
+
}
|
|
163121
163150
|
const cacheKey = JSON.stringify({
|
|
163122
163151
|
modelName,
|
|
163123
163152
|
clientOptions: mergedOptions
|
|
@@ -163126,7 +163155,10 @@ var V3 = (() => {
|
|
|
163126
163155
|
if (cached2) {
|
|
163127
163156
|
return cached2;
|
|
163128
163157
|
}
|
|
163129
|
-
const client = this.llmProvider.getClient(modelName, mergedOptions, {
|
|
163158
|
+
const client = this.llmProvider.getClient(modelName, mergedOptions, {
|
|
163159
|
+
experimental: this.experimental,
|
|
163160
|
+
disableAPI: this.disableAPI
|
|
163161
|
+
});
|
|
163130
163162
|
this.overrideLlmClients.set(cacheKey, client);
|
|
163131
163163
|
return client;
|
|
163132
163164
|
}
|
|
@@ -164620,9 +164652,32 @@ var import_child_process4 = require("child_process");
|
|
|
164620
164652
|
var readline = __toESM(require("readline"));
|
|
164621
164653
|
|
|
164622
164654
|
// package.json
|
|
164623
|
-
var version3 = "0.
|
|
164655
|
+
var version3 = "0.4.1";
|
|
164656
|
+
|
|
164657
|
+
// src/resolve-ws.ts
|
|
164658
|
+
init_cjs_shims();
|
|
164659
|
+
async function resolveWsTarget(input) {
|
|
164660
|
+
if (/^\d+$/.test(input)) {
|
|
164661
|
+
const port = input;
|
|
164662
|
+
const url2 = `http://127.0.0.1:${port}/json/version`;
|
|
164663
|
+
try {
|
|
164664
|
+
const res = await fetch(url2);
|
|
164665
|
+
if (!res.ok) {
|
|
164666
|
+
throw new Error(`HTTP ${res.status} from ${url2}`);
|
|
164667
|
+
}
|
|
164668
|
+
const json2 = await res.json();
|
|
164669
|
+
if (json2.webSocketDebuggerUrl) {
|
|
164670
|
+
return json2.webSocketDebuggerUrl;
|
|
164671
|
+
}
|
|
164672
|
+
} catch {
|
|
164673
|
+
}
|
|
164674
|
+
return `ws://127.0.0.1:${port}/devtools/browser`;
|
|
164675
|
+
}
|
|
164676
|
+
return input;
|
|
164677
|
+
}
|
|
164624
164678
|
|
|
164625
164679
|
// src/index.ts
|
|
164680
|
+
var import_node_html_markdown = require("node-html-markdown");
|
|
164626
164681
|
var program = new import_commander.Command();
|
|
164627
164682
|
var SOCKET_DIR = os3.tmpdir();
|
|
164628
164683
|
function getSocketPath(session) {
|
|
@@ -164717,6 +164772,34 @@ function getContextPath(session) {
|
|
|
164717
164772
|
function getConnectPath(session) {
|
|
164718
164773
|
return path11.join(SOCKET_DIR, `browse-${session}.connect`);
|
|
164719
164774
|
}
|
|
164775
|
+
function getLocalConfigPath(session) {
|
|
164776
|
+
return path11.join(SOCKET_DIR, `browse-${session}.local-config`);
|
|
164777
|
+
}
|
|
164778
|
+
function getLocalInfoPath(session) {
|
|
164779
|
+
return path11.join(SOCKET_DIR, `browse-${session}.local-info`);
|
|
164780
|
+
}
|
|
164781
|
+
async function readLocalConfig(session) {
|
|
164782
|
+
try {
|
|
164783
|
+
const raw = await import_fs11.promises.readFile(getLocalConfigPath(session), "utf-8");
|
|
164784
|
+
return JSON.parse(raw);
|
|
164785
|
+
} catch {
|
|
164786
|
+
return { strategy: "auto" };
|
|
164787
|
+
}
|
|
164788
|
+
}
|
|
164789
|
+
async function writeLocalConfig(session, config3) {
|
|
164790
|
+
await import_fs11.promises.writeFile(getLocalConfigPath(session), JSON.stringify(config3));
|
|
164791
|
+
}
|
|
164792
|
+
async function writeLocalInfo(session, info) {
|
|
164793
|
+
await import_fs11.promises.writeFile(getLocalInfoPath(session), JSON.stringify(info));
|
|
164794
|
+
}
|
|
164795
|
+
async function readLocalInfo(session) {
|
|
164796
|
+
try {
|
|
164797
|
+
const raw = await import_fs11.promises.readFile(getLocalInfoPath(session), "utf-8");
|
|
164798
|
+
return JSON.parse(raw);
|
|
164799
|
+
} catch {
|
|
164800
|
+
return null;
|
|
164801
|
+
}
|
|
164802
|
+
}
|
|
164720
164803
|
function hasBrowserbaseCredentials() {
|
|
164721
164804
|
return Boolean(process.env.BROWSERBASE_API_KEY);
|
|
164722
164805
|
}
|
|
@@ -164748,6 +164831,165 @@ async function getDesiredMode(session) {
|
|
|
164748
164831
|
}
|
|
164749
164832
|
return hasBrowserbaseCredentials() ? "browserbase" : "local";
|
|
164750
164833
|
}
|
|
164834
|
+
function getChromeUserDataDirs() {
|
|
164835
|
+
const home = os3.homedir();
|
|
164836
|
+
const dirs = [];
|
|
164837
|
+
if (process.platform === "darwin") {
|
|
164838
|
+
const base = path11.join(home, "Library", "Application Support");
|
|
164839
|
+
for (const name18 of [
|
|
164840
|
+
"Google/Chrome",
|
|
164841
|
+
"Google/Chrome Canary",
|
|
164842
|
+
"Chromium",
|
|
164843
|
+
"BraveSoftware/Brave-Browser"
|
|
164844
|
+
]) {
|
|
164845
|
+
dirs.push(path11.join(base, name18));
|
|
164846
|
+
}
|
|
164847
|
+
} else if (process.platform === "linux") {
|
|
164848
|
+
const config3 = path11.join(home, ".config");
|
|
164849
|
+
for (const name18 of [
|
|
164850
|
+
"google-chrome",
|
|
164851
|
+
"google-chrome-unstable",
|
|
164852
|
+
"chromium",
|
|
164853
|
+
"BraveSoftware/Brave-Browser"
|
|
164854
|
+
]) {
|
|
164855
|
+
dirs.push(path11.join(config3, name18));
|
|
164856
|
+
}
|
|
164857
|
+
}
|
|
164858
|
+
return dirs;
|
|
164859
|
+
}
|
|
164860
|
+
async function readDevToolsActivePort(userDataDir) {
|
|
164861
|
+
try {
|
|
164862
|
+
const content = await import_fs11.promises.readFile(
|
|
164863
|
+
path11.join(userDataDir, "DevToolsActivePort"),
|
|
164864
|
+
"utf-8"
|
|
164865
|
+
);
|
|
164866
|
+
const lines = content.trim().split("\n");
|
|
164867
|
+
const port = parseInt(lines[0]?.trim(), 10);
|
|
164868
|
+
if (isNaN(port) || port <= 0 || port > 65535) return null;
|
|
164869
|
+
const wsPath = lines[1]?.trim() || "/devtools/browser";
|
|
164870
|
+
return { port, wsPath };
|
|
164871
|
+
} catch {
|
|
164872
|
+
return null;
|
|
164873
|
+
}
|
|
164874
|
+
}
|
|
164875
|
+
function isPortReachable(port, timeoutMs = 500) {
|
|
164876
|
+
return new Promise((resolve4) => {
|
|
164877
|
+
const sock = net2.createConnection({ host: "127.0.0.1", port });
|
|
164878
|
+
const timer = setTimeout(() => {
|
|
164879
|
+
sock.destroy();
|
|
164880
|
+
resolve4(false);
|
|
164881
|
+
}, timeoutMs);
|
|
164882
|
+
sock.on("connect", () => {
|
|
164883
|
+
clearTimeout(timer);
|
|
164884
|
+
sock.destroy();
|
|
164885
|
+
resolve4(true);
|
|
164886
|
+
});
|
|
164887
|
+
sock.on("error", () => {
|
|
164888
|
+
clearTimeout(timer);
|
|
164889
|
+
resolve4(false);
|
|
164890
|
+
});
|
|
164891
|
+
});
|
|
164892
|
+
}
|
|
164893
|
+
async function probeCdpEndpoint(port) {
|
|
164894
|
+
try {
|
|
164895
|
+
const controller = new AbortController();
|
|
164896
|
+
const timer = setTimeout(() => controller.abort(), 2e3);
|
|
164897
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
164898
|
+
signal: controller.signal
|
|
164899
|
+
});
|
|
164900
|
+
clearTimeout(timer);
|
|
164901
|
+
if (res.ok) {
|
|
164902
|
+
const json2 = await res.json();
|
|
164903
|
+
if (json2.webSocketDebuggerUrl) {
|
|
164904
|
+
return json2.webSocketDebuggerUrl;
|
|
164905
|
+
}
|
|
164906
|
+
}
|
|
164907
|
+
} catch {
|
|
164908
|
+
}
|
|
164909
|
+
const wsUrl = `ws://127.0.0.1:${port}/devtools/browser`;
|
|
164910
|
+
try {
|
|
164911
|
+
const verified = await verifyCdpWebSocket(wsUrl);
|
|
164912
|
+
if (verified) return wsUrl;
|
|
164913
|
+
} catch {
|
|
164914
|
+
}
|
|
164915
|
+
return null;
|
|
164916
|
+
}
|
|
164917
|
+
function verifyCdpWebSocket(wsUrl) {
|
|
164918
|
+
return new Promise((resolve4) => {
|
|
164919
|
+
const url2 = new URL(wsUrl);
|
|
164920
|
+
const port = parseInt(url2.port) || 80;
|
|
164921
|
+
const wsKey = Buffer.from(
|
|
164922
|
+
Array.from({ length: 16 }, () => Math.floor(Math.random() * 256))
|
|
164923
|
+
).toString("base64");
|
|
164924
|
+
const sock = net2.createConnection({ host: url2.hostname, port });
|
|
164925
|
+
let response = "";
|
|
164926
|
+
const timer = setTimeout(() => {
|
|
164927
|
+
sock.destroy();
|
|
164928
|
+
resolve4(false);
|
|
164929
|
+
}, 2e3);
|
|
164930
|
+
sock.on("connect", () => {
|
|
164931
|
+
sock.write(
|
|
164932
|
+
`GET ${url2.pathname} HTTP/1.1\r
|
|
164933
|
+
Host: ${url2.hostname}:${port}\r
|
|
164934
|
+
Upgrade: websocket\r
|
|
164935
|
+
Connection: Upgrade\r
|
|
164936
|
+
Sec-WebSocket-Key: ${wsKey}\r
|
|
164937
|
+
Sec-WebSocket-Version: 13\r
|
|
164938
|
+
\r
|
|
164939
|
+
`
|
|
164940
|
+
);
|
|
164941
|
+
});
|
|
164942
|
+
sock.on("data", (data) => {
|
|
164943
|
+
response += data.toString();
|
|
164944
|
+
if (/^HTTP\/1\.[01] 101(?:\s|$)/.test(response)) {
|
|
164945
|
+
clearTimeout(timer);
|
|
164946
|
+
sock.destroy();
|
|
164947
|
+
resolve4(true);
|
|
164948
|
+
} else if (response.includes("\r\n\r\n")) {
|
|
164949
|
+
clearTimeout(timer);
|
|
164950
|
+
sock.destroy();
|
|
164951
|
+
resolve4(false);
|
|
164952
|
+
}
|
|
164953
|
+
});
|
|
164954
|
+
sock.on("error", () => {
|
|
164955
|
+
clearTimeout(timer);
|
|
164956
|
+
resolve4(false);
|
|
164957
|
+
});
|
|
164958
|
+
});
|
|
164959
|
+
}
|
|
164960
|
+
async function discoverLocalCdp() {
|
|
164961
|
+
const candidates = [];
|
|
164962
|
+
const userDataDirs = getChromeUserDataDirs();
|
|
164963
|
+
for (const dir of userDataDirs) {
|
|
164964
|
+
const info = await readDevToolsActivePort(dir);
|
|
164965
|
+
if (!info) continue;
|
|
164966
|
+
if (!await isPortReachable(info.port)) {
|
|
164967
|
+
try {
|
|
164968
|
+
await import_fs11.promises.unlink(path11.join(dir, "DevToolsActivePort"));
|
|
164969
|
+
} catch {
|
|
164970
|
+
}
|
|
164971
|
+
continue;
|
|
164972
|
+
}
|
|
164973
|
+
const wsUrl = await probeCdpEndpoint(info.port);
|
|
164974
|
+
if (wsUrl) {
|
|
164975
|
+
const name18 = path11.basename(dir);
|
|
164976
|
+
candidates.push({ wsUrl, source: `DevToolsActivePort (${name18})` });
|
|
164977
|
+
}
|
|
164978
|
+
}
|
|
164979
|
+
if (candidates.length === 0) {
|
|
164980
|
+
for (const port of [9222, 9229]) {
|
|
164981
|
+
if (!await isPortReachable(port)) continue;
|
|
164982
|
+
const wsUrl = await probeCdpEndpoint(port);
|
|
164983
|
+
if (wsUrl) {
|
|
164984
|
+
candidates.push({ wsUrl, source: `port ${port}` });
|
|
164985
|
+
}
|
|
164986
|
+
}
|
|
164987
|
+
}
|
|
164988
|
+
if (candidates.length > 1) {
|
|
164989
|
+
return null;
|
|
164990
|
+
}
|
|
164991
|
+
return candidates[0] ?? null;
|
|
164992
|
+
}
|
|
164751
164993
|
async function isDaemonRunning(session) {
|
|
164752
164994
|
try {
|
|
164753
164995
|
const pidFile = getPidPath(session);
|
|
@@ -164766,14 +165008,16 @@ var DAEMON_STATE_FILES = (session) => [
|
|
|
164766
165008
|
getWsPath(session),
|
|
164767
165009
|
getChromePidPath(session),
|
|
164768
165010
|
getLockPath(session),
|
|
164769
|
-
getModePath(session)
|
|
165011
|
+
getModePath(session),
|
|
165012
|
+
getLocalInfoPath(session)
|
|
164770
165013
|
];
|
|
164771
165014
|
async function cleanupStaleFiles(session) {
|
|
164772
165015
|
const files = [
|
|
164773
165016
|
...DAEMON_STATE_FILES(session),
|
|
164774
165017
|
// Client-written config, only cleaned on full shutdown
|
|
164775
165018
|
getContextPath(session),
|
|
164776
|
-
getConnectPath(session)
|
|
165019
|
+
getConnectPath(session),
|
|
165020
|
+
getLocalConfigPath(session)
|
|
164777
165021
|
];
|
|
164778
165022
|
for (const file2 of files) {
|
|
164779
165023
|
try {
|
|
@@ -164850,6 +165094,37 @@ async function runDaemon(session, headless) {
|
|
|
164850
165094
|
connectSessionId = (await import_fs11.promises.readFile(getConnectPath(session), "utf-8")).trim();
|
|
164851
165095
|
} catch {
|
|
164852
165096
|
}
|
|
165097
|
+
let localLaunchOptions;
|
|
165098
|
+
let localInfo;
|
|
165099
|
+
if (!useBrowserbase) {
|
|
165100
|
+
const localConfig = await readLocalConfig(session);
|
|
165101
|
+
if (localConfig.strategy === "isolated") {
|
|
165102
|
+
localLaunchOptions = { headless, viewport: DEFAULT_VIEWPORT2 };
|
|
165103
|
+
localInfo = { localSource: "isolated" };
|
|
165104
|
+
} else if (localConfig.strategy === "cdp") {
|
|
165105
|
+
const cdpUrl = await resolveWsTarget(localConfig.cdpTarget);
|
|
165106
|
+
localLaunchOptions = { cdpUrl };
|
|
165107
|
+
localInfo = {
|
|
165108
|
+
localSource: "attached-explicit",
|
|
165109
|
+
resolvedCdpUrl: cdpUrl
|
|
165110
|
+
};
|
|
165111
|
+
} else {
|
|
165112
|
+
const discovered = await discoverLocalCdp();
|
|
165113
|
+
if (discovered) {
|
|
165114
|
+
localLaunchOptions = { cdpUrl: discovered.wsUrl };
|
|
165115
|
+
localInfo = {
|
|
165116
|
+
localSource: "attached-existing",
|
|
165117
|
+
resolvedCdpUrl: discovered.wsUrl
|
|
165118
|
+
};
|
|
165119
|
+
} else {
|
|
165120
|
+
localLaunchOptions = { headless, viewport: DEFAULT_VIEWPORT2 };
|
|
165121
|
+
localInfo = {
|
|
165122
|
+
localSource: "isolated-fallback",
|
|
165123
|
+
fallbackReason: "no debuggable local browser found"
|
|
165124
|
+
};
|
|
165125
|
+
}
|
|
165126
|
+
}
|
|
165127
|
+
}
|
|
164853
165128
|
stagehand = new V3({
|
|
164854
165129
|
env: useBrowserbase ? "BROWSERBASE" : "LOCAL",
|
|
164855
165130
|
verbose: 0,
|
|
@@ -164862,7 +165137,7 @@ async function runDaemon(session, headless) {
|
|
|
164862
165137
|
} : {},
|
|
164863
165138
|
...!connectSessionId ? {
|
|
164864
165139
|
browserbaseSessionCreateParams: {
|
|
164865
|
-
userMetadata: {
|
|
165140
|
+
userMetadata: { browse_cli: "true" },
|
|
164866
165141
|
...contextConfig ? {
|
|
164867
165142
|
browserSettings: {
|
|
164868
165143
|
context: contextConfig
|
|
@@ -164871,13 +165146,13 @@ async function runDaemon(session, headless) {
|
|
|
164871
165146
|
}
|
|
164872
165147
|
} : {}
|
|
164873
165148
|
} : {
|
|
164874
|
-
localBrowserLaunchOptions:
|
|
164875
|
-
headless,
|
|
164876
|
-
viewport: DEFAULT_VIEWPORT2
|
|
164877
|
-
}
|
|
165149
|
+
localBrowserLaunchOptions: localLaunchOptions
|
|
164878
165150
|
}
|
|
164879
165151
|
});
|
|
164880
165152
|
await import_fs11.promises.writeFile(getModePath(session), desiredMode);
|
|
165153
|
+
if (localInfo) {
|
|
165154
|
+
await writeLocalInfo(session, localInfo);
|
|
165155
|
+
}
|
|
164881
165156
|
await stagehand.init();
|
|
164882
165157
|
context = stagehand.context;
|
|
164883
165158
|
context.conn.onTransportClosed(() => {
|
|
@@ -164935,7 +165210,7 @@ async function runDaemon(session, headless) {
|
|
|
164935
165210
|
}
|
|
164936
165211
|
} catch {
|
|
164937
165212
|
}
|
|
164938
|
-
await
|
|
165213
|
+
await cleanupDaemonStateFiles(session);
|
|
164939
165214
|
process.exit(0);
|
|
164940
165215
|
};
|
|
164941
165216
|
process.on("SIGTERM", () => shutdown());
|
|
@@ -165224,6 +165499,11 @@ async function executeCommand(context, command, args, stagehand) {
|
|
|
165224
165499
|
return {
|
|
165225
165500
|
checked: await page.deepLocator(resolveSelector(selector)).isChecked()
|
|
165226
165501
|
};
|
|
165502
|
+
case "markdown": {
|
|
165503
|
+
const target = selector ? resolveSelector(selector) : "body";
|
|
165504
|
+
const html = await page.deepLocator(target).innerHtml();
|
|
165505
|
+
return { markdown: import_node_html_markdown.NodeHtmlMarkdown.translate(html) };
|
|
165506
|
+
}
|
|
165227
165507
|
default:
|
|
165228
165508
|
throw new Error(`Unknown get type: ${what}`);
|
|
165229
165509
|
}
|
|
@@ -165602,7 +165882,7 @@ async function stopDaemonAndCleanup(session) {
|
|
|
165602
165882
|
} catch {
|
|
165603
165883
|
}
|
|
165604
165884
|
await new Promise((r3) => setTimeout(r3, 500));
|
|
165605
|
-
await
|
|
165885
|
+
await cleanupDaemonStateFiles(session);
|
|
165606
165886
|
}
|
|
165607
165887
|
async function ensureDaemon(session, headless) {
|
|
165608
165888
|
const wantMode = await getDesiredMode(session);
|
|
@@ -165689,12 +165969,13 @@ async function runCommand(command, args) {
|
|
|
165689
165969
|
const session = getSession(opts);
|
|
165690
165970
|
const headless = isHeadless(opts);
|
|
165691
165971
|
if (opts.ws) {
|
|
165972
|
+
const cdpUrl = await resolveWsTarget(opts.ws);
|
|
165692
165973
|
const stagehand = new V3({
|
|
165693
165974
|
env: "LOCAL",
|
|
165694
165975
|
verbose: 0,
|
|
165695
165976
|
disablePino: true,
|
|
165696
165977
|
localBrowserLaunchOptions: {
|
|
165697
|
-
cdpUrl
|
|
165978
|
+
cdpUrl
|
|
165698
165979
|
}
|
|
165699
165980
|
});
|
|
165700
165981
|
await stagehand.init();
|
|
@@ -165732,8 +166013,8 @@ async function runCommand(command, args) {
|
|
|
165732
166013
|
return sendCommand(session, command, args, headless);
|
|
165733
166014
|
}
|
|
165734
166015
|
program.name("browse").description("Browser automation CLI for AI agents").version(version3).option(
|
|
165735
|
-
"--ws <url>",
|
|
165736
|
-
"CDP WebSocket URL (bypasses daemon, direct connection)"
|
|
166016
|
+
"--ws <url|port>",
|
|
166017
|
+
"CDP WebSocket URL or port number (bypasses daemon, direct connection)"
|
|
165737
166018
|
).option("--headless", "Run Chrome in headless mode").option("--headed", "Run Chrome with visible window (default)").option("--json", "Output as JSON", false).option(
|
|
165738
166019
|
"--session <name>",
|
|
165739
166020
|
"Session name for multiple browsers (or use BROWSE_SESSION env var)"
|
|
@@ -165778,6 +166059,7 @@ program.command("status").description("Check daemon status").action(async () =>
|
|
|
165778
166059
|
let wsUrl = null;
|
|
165779
166060
|
let mode = null;
|
|
165780
166061
|
let browserbaseSessionId = null;
|
|
166062
|
+
let localDetails = {};
|
|
165781
166063
|
if (running) {
|
|
165782
166064
|
try {
|
|
165783
166065
|
wsUrl = await import_fs11.promises.readFile(getWsPath(session), "utf-8");
|
|
@@ -165788,68 +166070,106 @@ program.command("status").description("Check daemon status").action(async () =>
|
|
|
165788
166070
|
browserbaseSessionId = (await import_fs11.promises.readFile(getConnectPath(session), "utf-8")).trim();
|
|
165789
166071
|
} catch {
|
|
165790
166072
|
}
|
|
166073
|
+
if (mode === "local") {
|
|
166074
|
+
const localConfig = await readLocalConfig(session);
|
|
166075
|
+
const localInfo = await readLocalInfo(session);
|
|
166076
|
+
localDetails = {
|
|
166077
|
+
localStrategy: localConfig.strategy,
|
|
166078
|
+
...localInfo ?? {}
|
|
166079
|
+
};
|
|
166080
|
+
}
|
|
165791
166081
|
}
|
|
165792
166082
|
console.log(
|
|
165793
|
-
JSON.stringify({
|
|
166083
|
+
JSON.stringify({
|
|
166084
|
+
running,
|
|
166085
|
+
session,
|
|
166086
|
+
wsUrl,
|
|
166087
|
+
mode,
|
|
166088
|
+
browserbaseSessionId,
|
|
166089
|
+
...localDetails
|
|
166090
|
+
})
|
|
165794
166091
|
);
|
|
165795
166092
|
});
|
|
165796
|
-
program.command("env [target]").description(
|
|
165797
|
-
|
|
165798
|
-
|
|
165799
|
-
|
|
165800
|
-
|
|
165801
|
-
const
|
|
165802
|
-
if (
|
|
165803
|
-
mode =
|
|
165804
|
-
|
|
165805
|
-
|
|
165806
|
-
|
|
165807
|
-
|
|
165808
|
-
|
|
165809
|
-
|
|
165810
|
-
})
|
|
165811
|
-
);
|
|
165812
|
-
return;
|
|
165813
|
-
}
|
|
165814
|
-
const modeMap = {
|
|
165815
|
-
local: "local",
|
|
165816
|
-
remote: "browserbase"
|
|
165817
|
-
};
|
|
165818
|
-
const mapped = modeMap[target];
|
|
165819
|
-
if (!mapped) {
|
|
165820
|
-
console.error("Usage: browse env [local|remote]");
|
|
165821
|
-
process.exit(1);
|
|
165822
|
-
}
|
|
165823
|
-
try {
|
|
165824
|
-
assertModeSupported(mapped);
|
|
165825
|
-
} catch (err) {
|
|
165826
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
165827
|
-
process.exit(1);
|
|
165828
|
-
}
|
|
165829
|
-
await import_fs11.promises.writeFile(getModeOverridePath(session), mapped);
|
|
165830
|
-
if (await isDaemonRunning(session)) {
|
|
165831
|
-
const currentMode = await readCurrentMode(session) ?? "local";
|
|
165832
|
-
if (currentMode === mapped) {
|
|
166093
|
+
program.command("env [target] [cdpTarget]").description(
|
|
166094
|
+
"Show or switch browser environment (local | remote)\n\n browse env Show current environment\n browse env local Auto-discover local Chrome, fallback to isolated\n browse env local --isolated Force clean isolated browser\n browse env local <port|url> Attach to specific CDP target\n browse env remote Use Browserbase (requires API key)"
|
|
166095
|
+
).option("--isolated", "Force isolated local browser (no auto-discovery)").action(
|
|
166096
|
+
async (target, cdpTarget, cmdOpts) => {
|
|
166097
|
+
const opts = program.opts();
|
|
166098
|
+
const session = getSession(opts);
|
|
166099
|
+
if (!target) {
|
|
166100
|
+
let mode = null;
|
|
166101
|
+
const desiredMode = await getDesiredMode(session);
|
|
166102
|
+
const localConfig2 = await readLocalConfig(session);
|
|
166103
|
+
const localInfo = await readLocalInfo(session);
|
|
166104
|
+
if (await isDaemonRunning(session)) {
|
|
166105
|
+
mode = toModeTarget(await readCurrentMode(session) ?? desiredMode);
|
|
166106
|
+
}
|
|
165833
166107
|
console.log(
|
|
165834
166108
|
JSON.stringify({
|
|
165835
|
-
mode:
|
|
166109
|
+
mode: mode ?? "not running",
|
|
166110
|
+
desired: toModeTarget(desiredMode),
|
|
165836
166111
|
session,
|
|
165837
|
-
|
|
166112
|
+
...desiredMode === "local" ? {
|
|
166113
|
+
localStrategy: localConfig2.strategy,
|
|
166114
|
+
...localInfo ?? {}
|
|
166115
|
+
} : {}
|
|
165838
166116
|
})
|
|
165839
166117
|
);
|
|
165840
166118
|
return;
|
|
165841
166119
|
}
|
|
165842
|
-
|
|
166120
|
+
const modeMap = {
|
|
166121
|
+
local: "local",
|
|
166122
|
+
remote: "browserbase"
|
|
166123
|
+
};
|
|
166124
|
+
const mapped = modeMap[target];
|
|
166125
|
+
if (!mapped) {
|
|
166126
|
+
console.error(
|
|
166127
|
+
"Usage: browse env [local|remote]\n browse env local [--isolated] [<port|url>]"
|
|
166128
|
+
);
|
|
166129
|
+
process.exit(1);
|
|
166130
|
+
}
|
|
166131
|
+
try {
|
|
166132
|
+
assertModeSupported(mapped);
|
|
166133
|
+
} catch (err) {
|
|
166134
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
166135
|
+
process.exit(1);
|
|
166136
|
+
}
|
|
166137
|
+
let localConfig = { strategy: "auto" };
|
|
166138
|
+
if (mapped === "local") {
|
|
166139
|
+
if (cmdOpts.isolated) {
|
|
166140
|
+
localConfig = { strategy: "isolated" };
|
|
166141
|
+
} else if (cdpTarget) {
|
|
166142
|
+
localConfig = { strategy: "cdp", cdpTarget };
|
|
166143
|
+
}
|
|
166144
|
+
await writeLocalConfig(session, localConfig);
|
|
166145
|
+
}
|
|
166146
|
+
await import_fs11.promises.writeFile(getModeOverridePath(session), mapped);
|
|
166147
|
+
if (await isDaemonRunning(session)) {
|
|
166148
|
+
const currentMode = await readCurrentMode(session) ?? "local";
|
|
166149
|
+
const needsRestart = currentMode !== mapped || mapped === "local";
|
|
166150
|
+
if (!needsRestart) {
|
|
166151
|
+
console.log(
|
|
166152
|
+
JSON.stringify({
|
|
166153
|
+
mode: toModeTarget(mapped),
|
|
166154
|
+
session,
|
|
166155
|
+
restarted: false
|
|
166156
|
+
})
|
|
166157
|
+
);
|
|
166158
|
+
return;
|
|
166159
|
+
}
|
|
166160
|
+
await stopDaemonAndCleanup(session);
|
|
166161
|
+
}
|
|
166162
|
+
await ensureDaemon(session, isHeadless(opts));
|
|
166163
|
+
console.log(
|
|
166164
|
+
JSON.stringify({
|
|
166165
|
+
mode: toModeTarget(mapped),
|
|
166166
|
+
session,
|
|
166167
|
+
restarted: true,
|
|
166168
|
+
...mapped === "local" ? { localStrategy: localConfig.strategy } : {}
|
|
166169
|
+
})
|
|
166170
|
+
);
|
|
165843
166171
|
}
|
|
165844
|
-
|
|
165845
|
-
console.log(
|
|
165846
|
-
JSON.stringify({
|
|
165847
|
-
mode: toModeTarget(mapped),
|
|
165848
|
-
session,
|
|
165849
|
-
restarted: true
|
|
165850
|
-
})
|
|
165851
|
-
);
|
|
165852
|
-
});
|
|
166172
|
+
);
|
|
165853
166173
|
program.command("refs").description("Show cached ref map from last snapshot").action(async () => {
|
|
165854
166174
|
const opts = program.opts();
|
|
165855
166175
|
try {
|
|
@@ -166113,7 +166433,7 @@ program.command("highlight <selector>").description("Highlight element").option(
|
|
|
166113
166433
|
}
|
|
166114
166434
|
});
|
|
166115
166435
|
program.command("get <what> [selector]").description(
|
|
166116
|
-
"Get page info: url, title, text, html, value, box, visible, checked"
|
|
166436
|
+
"Get page info: url, title, text, html, markdown, value, box, visible, checked"
|
|
166117
166437
|
).action(async (what, selector) => {
|
|
166118
166438
|
const opts = program.opts();
|
|
166119
166439
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbasehq/browse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1-alpha-3917df4",
|
|
4
4
|
"description": "Browser automation CLI for AI agents, built on Stagehand",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,25 +41,14 @@
|
|
|
41
41
|
"README.md",
|
|
42
42
|
"LICENSE"
|
|
43
43
|
],
|
|
44
|
-
"scripts": {
|
|
45
|
-
"build": "tsup",
|
|
46
|
-
"dev": "tsx src/index.ts",
|
|
47
|
-
"browse": "tsx src/index.ts",
|
|
48
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
-
"eslint": "eslint .",
|
|
50
|
-
"lint": "cd ../.. && prettier --check packages/cli && cd packages/cli && pnpm run eslint && pnpm run typecheck",
|
|
51
|
-
"test": "vitest run",
|
|
52
|
-
"test:cli": "vitest run",
|
|
53
|
-
"test:watch": "vitest",
|
|
54
|
-
"prepublishOnly": "pnpm run build"
|
|
55
|
-
},
|
|
56
44
|
"dependencies": {
|
|
57
|
-
"@browserbasehq/stagehand": "workspace:*",
|
|
58
45
|
"commander": "^12.0.0",
|
|
59
46
|
"dotenv": "^16.4.5",
|
|
47
|
+
"node-html-markdown": "^1.3.0",
|
|
60
48
|
"pino": "^9.6.0",
|
|
61
49
|
"pino-pretty": "^13.0.0",
|
|
62
|
-
"ws": "^8.18.0"
|
|
50
|
+
"ws": "^8.18.0",
|
|
51
|
+
"@browserbasehq/stagehand": "3.2.0"
|
|
63
52
|
},
|
|
64
53
|
"devDependencies": {
|
|
65
54
|
"@types/node": "^20.11.30",
|
|
@@ -69,5 +58,16 @@
|
|
|
69
58
|
"tsx": "^4.10.5",
|
|
70
59
|
"typescript": "5.8.3",
|
|
71
60
|
"vitest": "^4.0.8"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"dev": "tsx src/index.ts",
|
|
65
|
+
"browse": "tsx src/index.ts",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"eslint": "eslint .",
|
|
68
|
+
"lint": "cd ../.. && prettier --check packages/cli && cd packages/cli && pnpm run eslint && pnpm run typecheck",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:cli": "vitest run",
|
|
71
|
+
"test:watch": "vitest"
|
|
72
72
|
}
|
|
73
|
-
}
|
|
73
|
+
}
|