@browserbasehq/browse-cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -6
  3. package/dist/index.js +366 -78
  4. package/package.json +15 -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
- Behavior details:
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>` | Connect to existing Chrome via CDP WebSocket |
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
- # Start Chrome with remote debugging
254
- google-chrome --remote-debugging-port=9222
297
+ # Auto-discover Chrome with remote debugging enabled
298
+ browse env local
299
+ browse open https://example.com
255
300
 
256
- # Connect via WebSocket
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@1.21.7_postcss@8.5.6_tsx@4.19.4_typescript@5.8.3_yaml@2.7.1/node_modules/tsup/assets/cjs_shims.js
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@1.21.7_postcss@8.5.6_tsx@4.19.4_typescript@5.8.3_yaml@2.7.1/node_modules/tsup/assets/cjs_shims.js"() {
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
- if (variables && Object.keys(variables).length > 0) {
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
- if (variables && Object.keys(variables).length > 0) {
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
@@ -160612,7 +160616,9 @@ function hasInjectableDOM(url2) {
160612
160616
  return false;
160613
160617
  }
160614
160618
  function isNonWebTarget(info) {
160615
- return info.type !== "page" && info.type !== "iframe" || !hasInjectableDOM(info.url);
160619
+ if (info.type === "page")
160620
+ return false;
160621
+ return info.type !== "iframe" || !hasInjectableDOM(info.url);
160616
160622
  }
160617
160623
  function isTopLevelPage(info) {
160618
160624
  const ti = info;
@@ -164620,7 +164626,29 @@ var import_child_process4 = require("child_process");
164620
164626
  var readline = __toESM(require("readline"));
164621
164627
 
164622
164628
  // package.json
164623
- var version3 = "0.3.0";
164629
+ var version3 = "0.4.1";
164630
+
164631
+ // src/resolve-ws.ts
164632
+ init_cjs_shims();
164633
+ async function resolveWsTarget(input) {
164634
+ if (/^\d+$/.test(input)) {
164635
+ const port = input;
164636
+ const url2 = `http://127.0.0.1:${port}/json/version`;
164637
+ try {
164638
+ const res = await fetch(url2);
164639
+ if (!res.ok) {
164640
+ throw new Error(`HTTP ${res.status} from ${url2}`);
164641
+ }
164642
+ const json2 = await res.json();
164643
+ if (json2.webSocketDebuggerUrl) {
164644
+ return json2.webSocketDebuggerUrl;
164645
+ }
164646
+ } catch {
164647
+ }
164648
+ return `ws://127.0.0.1:${port}/devtools/browser`;
164649
+ }
164650
+ return input;
164651
+ }
164624
164652
 
164625
164653
  // src/index.ts
164626
164654
  var program = new import_commander.Command();
@@ -164717,6 +164745,34 @@ function getContextPath(session) {
164717
164745
  function getConnectPath(session) {
164718
164746
  return path11.join(SOCKET_DIR, `browse-${session}.connect`);
164719
164747
  }
164748
+ function getLocalConfigPath(session) {
164749
+ return path11.join(SOCKET_DIR, `browse-${session}.local-config`);
164750
+ }
164751
+ function getLocalInfoPath(session) {
164752
+ return path11.join(SOCKET_DIR, `browse-${session}.local-info`);
164753
+ }
164754
+ async function readLocalConfig(session) {
164755
+ try {
164756
+ const raw = await import_fs11.promises.readFile(getLocalConfigPath(session), "utf-8");
164757
+ return JSON.parse(raw);
164758
+ } catch {
164759
+ return { strategy: "auto" };
164760
+ }
164761
+ }
164762
+ async function writeLocalConfig(session, config3) {
164763
+ await import_fs11.promises.writeFile(getLocalConfigPath(session), JSON.stringify(config3));
164764
+ }
164765
+ async function writeLocalInfo(session, info) {
164766
+ await import_fs11.promises.writeFile(getLocalInfoPath(session), JSON.stringify(info));
164767
+ }
164768
+ async function readLocalInfo(session) {
164769
+ try {
164770
+ const raw = await import_fs11.promises.readFile(getLocalInfoPath(session), "utf-8");
164771
+ return JSON.parse(raw);
164772
+ } catch {
164773
+ return null;
164774
+ }
164775
+ }
164720
164776
  function hasBrowserbaseCredentials() {
164721
164777
  return Boolean(process.env.BROWSERBASE_API_KEY);
164722
164778
  }
@@ -164748,6 +164804,165 @@ async function getDesiredMode(session) {
164748
164804
  }
164749
164805
  return hasBrowserbaseCredentials() ? "browserbase" : "local";
164750
164806
  }
164807
+ function getChromeUserDataDirs() {
164808
+ const home = os3.homedir();
164809
+ const dirs = [];
164810
+ if (process.platform === "darwin") {
164811
+ const base = path11.join(home, "Library", "Application Support");
164812
+ for (const name18 of [
164813
+ "Google/Chrome",
164814
+ "Google/Chrome Canary",
164815
+ "Chromium",
164816
+ "BraveSoftware/Brave-Browser"
164817
+ ]) {
164818
+ dirs.push(path11.join(base, name18));
164819
+ }
164820
+ } else if (process.platform === "linux") {
164821
+ const config3 = path11.join(home, ".config");
164822
+ for (const name18 of [
164823
+ "google-chrome",
164824
+ "google-chrome-unstable",
164825
+ "chromium",
164826
+ "BraveSoftware/Brave-Browser"
164827
+ ]) {
164828
+ dirs.push(path11.join(config3, name18));
164829
+ }
164830
+ }
164831
+ return dirs;
164832
+ }
164833
+ async function readDevToolsActivePort(userDataDir) {
164834
+ try {
164835
+ const content = await import_fs11.promises.readFile(
164836
+ path11.join(userDataDir, "DevToolsActivePort"),
164837
+ "utf-8"
164838
+ );
164839
+ const lines = content.trim().split("\n");
164840
+ const port = parseInt(lines[0]?.trim(), 10);
164841
+ if (isNaN(port) || port <= 0 || port > 65535) return null;
164842
+ const wsPath = lines[1]?.trim() || "/devtools/browser";
164843
+ return { port, wsPath };
164844
+ } catch {
164845
+ return null;
164846
+ }
164847
+ }
164848
+ function isPortReachable(port, timeoutMs = 500) {
164849
+ return new Promise((resolve4) => {
164850
+ const sock = net2.createConnection({ host: "127.0.0.1", port });
164851
+ const timer = setTimeout(() => {
164852
+ sock.destroy();
164853
+ resolve4(false);
164854
+ }, timeoutMs);
164855
+ sock.on("connect", () => {
164856
+ clearTimeout(timer);
164857
+ sock.destroy();
164858
+ resolve4(true);
164859
+ });
164860
+ sock.on("error", () => {
164861
+ clearTimeout(timer);
164862
+ resolve4(false);
164863
+ });
164864
+ });
164865
+ }
164866
+ async function probeCdpEndpoint(port) {
164867
+ try {
164868
+ const controller = new AbortController();
164869
+ const timer = setTimeout(() => controller.abort(), 2e3);
164870
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
164871
+ signal: controller.signal
164872
+ });
164873
+ clearTimeout(timer);
164874
+ if (res.ok) {
164875
+ const json2 = await res.json();
164876
+ if (json2.webSocketDebuggerUrl) {
164877
+ return json2.webSocketDebuggerUrl;
164878
+ }
164879
+ }
164880
+ } catch {
164881
+ }
164882
+ const wsUrl = `ws://127.0.0.1:${port}/devtools/browser`;
164883
+ try {
164884
+ const verified = await verifyCdpWebSocket(wsUrl);
164885
+ if (verified) return wsUrl;
164886
+ } catch {
164887
+ }
164888
+ return null;
164889
+ }
164890
+ function verifyCdpWebSocket(wsUrl) {
164891
+ return new Promise((resolve4) => {
164892
+ const url2 = new URL(wsUrl);
164893
+ const port = parseInt(url2.port) || 80;
164894
+ const wsKey = Buffer.from(
164895
+ Array.from({ length: 16 }, () => Math.floor(Math.random() * 256))
164896
+ ).toString("base64");
164897
+ const sock = net2.createConnection({ host: url2.hostname, port });
164898
+ let response = "";
164899
+ const timer = setTimeout(() => {
164900
+ sock.destroy();
164901
+ resolve4(false);
164902
+ }, 2e3);
164903
+ sock.on("connect", () => {
164904
+ sock.write(
164905
+ `GET ${url2.pathname} HTTP/1.1\r
164906
+ Host: ${url2.hostname}:${port}\r
164907
+ Upgrade: websocket\r
164908
+ Connection: Upgrade\r
164909
+ Sec-WebSocket-Key: ${wsKey}\r
164910
+ Sec-WebSocket-Version: 13\r
164911
+ \r
164912
+ `
164913
+ );
164914
+ });
164915
+ sock.on("data", (data) => {
164916
+ response += data.toString();
164917
+ if (/^HTTP\/1\.[01] 101(?:\s|$)/.test(response)) {
164918
+ clearTimeout(timer);
164919
+ sock.destroy();
164920
+ resolve4(true);
164921
+ } else if (response.includes("\r\n\r\n")) {
164922
+ clearTimeout(timer);
164923
+ sock.destroy();
164924
+ resolve4(false);
164925
+ }
164926
+ });
164927
+ sock.on("error", () => {
164928
+ clearTimeout(timer);
164929
+ resolve4(false);
164930
+ });
164931
+ });
164932
+ }
164933
+ async function discoverLocalCdp() {
164934
+ const candidates = [];
164935
+ const userDataDirs = getChromeUserDataDirs();
164936
+ for (const dir of userDataDirs) {
164937
+ const info = await readDevToolsActivePort(dir);
164938
+ if (!info) continue;
164939
+ if (!await isPortReachable(info.port)) {
164940
+ try {
164941
+ await import_fs11.promises.unlink(path11.join(dir, "DevToolsActivePort"));
164942
+ } catch {
164943
+ }
164944
+ continue;
164945
+ }
164946
+ const wsUrl = await probeCdpEndpoint(info.port);
164947
+ if (wsUrl) {
164948
+ const name18 = path11.basename(dir);
164949
+ candidates.push({ wsUrl, source: `DevToolsActivePort (${name18})` });
164950
+ }
164951
+ }
164952
+ if (candidates.length === 0) {
164953
+ for (const port of [9222, 9229]) {
164954
+ if (!await isPortReachable(port)) continue;
164955
+ const wsUrl = await probeCdpEndpoint(port);
164956
+ if (wsUrl) {
164957
+ candidates.push({ wsUrl, source: `port ${port}` });
164958
+ }
164959
+ }
164960
+ }
164961
+ if (candidates.length > 1) {
164962
+ return null;
164963
+ }
164964
+ return candidates[0] ?? null;
164965
+ }
164751
164966
  async function isDaemonRunning(session) {
164752
164967
  try {
164753
164968
  const pidFile = getPidPath(session);
@@ -164766,14 +164981,16 @@ var DAEMON_STATE_FILES = (session) => [
164766
164981
  getWsPath(session),
164767
164982
  getChromePidPath(session),
164768
164983
  getLockPath(session),
164769
- getModePath(session)
164984
+ getModePath(session),
164985
+ getLocalInfoPath(session)
164770
164986
  ];
164771
164987
  async function cleanupStaleFiles(session) {
164772
164988
  const files = [
164773
164989
  ...DAEMON_STATE_FILES(session),
164774
164990
  // Client-written config, only cleaned on full shutdown
164775
164991
  getContextPath(session),
164776
- getConnectPath(session)
164992
+ getConnectPath(session),
164993
+ getLocalConfigPath(session)
164777
164994
  ];
164778
164995
  for (const file2 of files) {
164779
164996
  try {
@@ -164850,6 +165067,37 @@ async function runDaemon(session, headless) {
164850
165067
  connectSessionId = (await import_fs11.promises.readFile(getConnectPath(session), "utf-8")).trim();
164851
165068
  } catch {
164852
165069
  }
165070
+ let localLaunchOptions;
165071
+ let localInfo;
165072
+ if (!useBrowserbase) {
165073
+ const localConfig = await readLocalConfig(session);
165074
+ if (localConfig.strategy === "isolated") {
165075
+ localLaunchOptions = { headless, viewport: DEFAULT_VIEWPORT2 };
165076
+ localInfo = { localSource: "isolated" };
165077
+ } else if (localConfig.strategy === "cdp") {
165078
+ const cdpUrl = await resolveWsTarget(localConfig.cdpTarget);
165079
+ localLaunchOptions = { cdpUrl };
165080
+ localInfo = {
165081
+ localSource: "attached-explicit",
165082
+ resolvedCdpUrl: cdpUrl
165083
+ };
165084
+ } else {
165085
+ const discovered = await discoverLocalCdp();
165086
+ if (discovered) {
165087
+ localLaunchOptions = { cdpUrl: discovered.wsUrl };
165088
+ localInfo = {
165089
+ localSource: "attached-existing",
165090
+ resolvedCdpUrl: discovered.wsUrl
165091
+ };
165092
+ } else {
165093
+ localLaunchOptions = { headless, viewport: DEFAULT_VIEWPORT2 };
165094
+ localInfo = {
165095
+ localSource: "isolated-fallback",
165096
+ fallbackReason: "no debuggable local browser found"
165097
+ };
165098
+ }
165099
+ }
165100
+ }
164853
165101
  stagehand = new V3({
164854
165102
  env: useBrowserbase ? "BROWSERBASE" : "LOCAL",
164855
165103
  verbose: 0,
@@ -164862,7 +165110,7 @@ async function runDaemon(session, headless) {
164862
165110
  } : {},
164863
165111
  ...!connectSessionId ? {
164864
165112
  browserbaseSessionCreateParams: {
164865
- userMetadata: { "browse-cli": "true" },
165113
+ userMetadata: { browse_cli: "true" },
164866
165114
  ...contextConfig ? {
164867
165115
  browserSettings: {
164868
165116
  context: contextConfig
@@ -164871,13 +165119,13 @@ async function runDaemon(session, headless) {
164871
165119
  }
164872
165120
  } : {}
164873
165121
  } : {
164874
- localBrowserLaunchOptions: {
164875
- headless,
164876
- viewport: DEFAULT_VIEWPORT2
164877
- }
165122
+ localBrowserLaunchOptions: localLaunchOptions
164878
165123
  }
164879
165124
  });
164880
165125
  await import_fs11.promises.writeFile(getModePath(session), desiredMode);
165126
+ if (localInfo) {
165127
+ await writeLocalInfo(session, localInfo);
165128
+ }
164881
165129
  await stagehand.init();
164882
165130
  context = stagehand.context;
164883
165131
  context.conn.onTransportClosed(() => {
@@ -164935,7 +165183,7 @@ async function runDaemon(session, headless) {
164935
165183
  }
164936
165184
  } catch {
164937
165185
  }
164938
- await cleanupStaleFiles(session);
165186
+ await cleanupDaemonStateFiles(session);
164939
165187
  process.exit(0);
164940
165188
  };
164941
165189
  process.on("SIGTERM", () => shutdown());
@@ -165602,7 +165850,7 @@ async function stopDaemonAndCleanup(session) {
165602
165850
  } catch {
165603
165851
  }
165604
165852
  await new Promise((r3) => setTimeout(r3, 500));
165605
- await cleanupStaleFiles(session);
165853
+ await cleanupDaemonStateFiles(session);
165606
165854
  }
165607
165855
  async function ensureDaemon(session, headless) {
165608
165856
  const wantMode = await getDesiredMode(session);
@@ -165689,12 +165937,13 @@ async function runCommand(command, args) {
165689
165937
  const session = getSession(opts);
165690
165938
  const headless = isHeadless(opts);
165691
165939
  if (opts.ws) {
165940
+ const cdpUrl = await resolveWsTarget(opts.ws);
165692
165941
  const stagehand = new V3({
165693
165942
  env: "LOCAL",
165694
165943
  verbose: 0,
165695
165944
  disablePino: true,
165696
165945
  localBrowserLaunchOptions: {
165697
- cdpUrl: opts.ws
165946
+ cdpUrl
165698
165947
  }
165699
165948
  });
165700
165949
  await stagehand.init();
@@ -165732,8 +165981,8 @@ async function runCommand(command, args) {
165732
165981
  return sendCommand(session, command, args, headless);
165733
165982
  }
165734
165983
  program.name("browse").description("Browser automation CLI for AI agents").version(version3).option(
165735
- "--ws <url>",
165736
- "CDP WebSocket URL (bypasses daemon, direct connection)"
165984
+ "--ws <url|port>",
165985
+ "CDP WebSocket URL or port number (bypasses daemon, direct connection)"
165737
165986
  ).option("--headless", "Run Chrome in headless mode").option("--headed", "Run Chrome with visible window (default)").option("--json", "Output as JSON", false).option(
165738
165987
  "--session <name>",
165739
165988
  "Session name for multiple browsers (or use BROWSE_SESSION env var)"
@@ -165778,6 +166027,7 @@ program.command("status").description("Check daemon status").action(async () =>
165778
166027
  let wsUrl = null;
165779
166028
  let mode = null;
165780
166029
  let browserbaseSessionId = null;
166030
+ let localDetails = {};
165781
166031
  if (running) {
165782
166032
  try {
165783
166033
  wsUrl = await import_fs11.promises.readFile(getWsPath(session), "utf-8");
@@ -165788,68 +166038,106 @@ program.command("status").description("Check daemon status").action(async () =>
165788
166038
  browserbaseSessionId = (await import_fs11.promises.readFile(getConnectPath(session), "utf-8")).trim();
165789
166039
  } catch {
165790
166040
  }
166041
+ if (mode === "local") {
166042
+ const localConfig = await readLocalConfig(session);
166043
+ const localInfo = await readLocalInfo(session);
166044
+ localDetails = {
166045
+ localStrategy: localConfig.strategy,
166046
+ ...localInfo ?? {}
166047
+ };
166048
+ }
165791
166049
  }
165792
166050
  console.log(
165793
- JSON.stringify({ running, session, wsUrl, mode, browserbaseSessionId })
166051
+ JSON.stringify({
166052
+ running,
166053
+ session,
166054
+ wsUrl,
166055
+ mode,
166056
+ browserbaseSessionId,
166057
+ ...localDetails
166058
+ })
165794
166059
  );
165795
166060
  });
165796
- program.command("env [target]").description("Show or switch browser environment (local | remote)").action(async (target) => {
165797
- const opts = program.opts();
165798
- const session = getSession(opts);
165799
- if (!target) {
165800
- let mode = null;
165801
- const desiredMode = await getDesiredMode(session);
165802
- if (await isDaemonRunning(session)) {
165803
- mode = toModeTarget(await readCurrentMode(session) ?? desiredMode);
165804
- }
165805
- console.log(
165806
- JSON.stringify({
165807
- mode: mode ?? "not running",
165808
- desired: toModeTarget(desiredMode),
165809
- session
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) {
166061
+ program.command("env [target] [cdpTarget]").description(
166062
+ "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)"
166063
+ ).option("--isolated", "Force isolated local browser (no auto-discovery)").action(
166064
+ async (target, cdpTarget, cmdOpts) => {
166065
+ const opts = program.opts();
166066
+ const session = getSession(opts);
166067
+ if (!target) {
166068
+ let mode = null;
166069
+ const desiredMode = await getDesiredMode(session);
166070
+ const localConfig2 = await readLocalConfig(session);
166071
+ const localInfo = await readLocalInfo(session);
166072
+ if (await isDaemonRunning(session)) {
166073
+ mode = toModeTarget(await readCurrentMode(session) ?? desiredMode);
166074
+ }
165833
166075
  console.log(
165834
166076
  JSON.stringify({
165835
- mode: toModeTarget(mapped),
166077
+ mode: mode ?? "not running",
166078
+ desired: toModeTarget(desiredMode),
165836
166079
  session,
165837
- restarted: false
166080
+ ...desiredMode === "local" ? {
166081
+ localStrategy: localConfig2.strategy,
166082
+ ...localInfo ?? {}
166083
+ } : {}
165838
166084
  })
165839
166085
  );
165840
166086
  return;
165841
166087
  }
165842
- await stopDaemonAndCleanup(session);
166088
+ const modeMap = {
166089
+ local: "local",
166090
+ remote: "browserbase"
166091
+ };
166092
+ const mapped = modeMap[target];
166093
+ if (!mapped) {
166094
+ console.error(
166095
+ "Usage: browse env [local|remote]\n browse env local [--isolated] [<port|url>]"
166096
+ );
166097
+ process.exit(1);
166098
+ }
166099
+ try {
166100
+ assertModeSupported(mapped);
166101
+ } catch (err) {
166102
+ console.error(err instanceof Error ? err.message : String(err));
166103
+ process.exit(1);
166104
+ }
166105
+ let localConfig = { strategy: "auto" };
166106
+ if (mapped === "local") {
166107
+ if (cmdOpts.isolated) {
166108
+ localConfig = { strategy: "isolated" };
166109
+ } else if (cdpTarget) {
166110
+ localConfig = { strategy: "cdp", cdpTarget };
166111
+ }
166112
+ await writeLocalConfig(session, localConfig);
166113
+ }
166114
+ await import_fs11.promises.writeFile(getModeOverridePath(session), mapped);
166115
+ if (await isDaemonRunning(session)) {
166116
+ const currentMode = await readCurrentMode(session) ?? "local";
166117
+ const needsRestart = currentMode !== mapped || mapped === "local";
166118
+ if (!needsRestart) {
166119
+ console.log(
166120
+ JSON.stringify({
166121
+ mode: toModeTarget(mapped),
166122
+ session,
166123
+ restarted: false
166124
+ })
166125
+ );
166126
+ return;
166127
+ }
166128
+ await stopDaemonAndCleanup(session);
166129
+ }
166130
+ await ensureDaemon(session, isHeadless(opts));
166131
+ console.log(
166132
+ JSON.stringify({
166133
+ mode: toModeTarget(mapped),
166134
+ session,
166135
+ restarted: true,
166136
+ ...mapped === "local" ? { localStrategy: localConfig.strategy } : {}
166137
+ })
166138
+ );
165843
166139
  }
165844
- await ensureDaemon(session, isHeadless(opts));
165845
- console.log(
165846
- JSON.stringify({
165847
- mode: toModeTarget(mapped),
165848
- session,
165849
- restarted: true
165850
- })
165851
- );
165852
- });
166140
+ );
165853
166141
  program.command("refs").description("Show cached ref map from last snapshot").action(async () => {
165854
166142
  const opts = program.opts();
165855
166143
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbasehq/browse-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Browser automation CLI for AI agents, built on Stagehand",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
@@ -41,25 +41,13 @@
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",
60
47
  "pino": "^9.6.0",
61
48
  "pino-pretty": "^13.0.0",
62
- "ws": "^8.18.0"
49
+ "ws": "^8.18.0",
50
+ "@browserbasehq/stagehand": "3.2.1-alpha-ad055f91530a84bd5d0735f7d5be82912580914b"
63
51
  },
64
52
  "devDependencies": {
65
53
  "@types/node": "^20.11.30",
@@ -69,5 +57,16 @@
69
57
  "tsx": "^4.10.5",
70
58
  "typescript": "5.8.3",
71
59
  "vitest": "^4.0.8"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "dev": "tsx src/index.ts",
64
+ "browse": "tsx src/index.ts",
65
+ "typecheck": "tsc --noEmit",
66
+ "eslint": "eslint .",
67
+ "lint": "cd ../.. && prettier --check packages/cli && cd packages/cli && pnpm run eslint && pnpm run typecheck",
68
+ "test": "vitest run",
69
+ "test:cli": "vitest run",
70
+ "test:watch": "vitest"
72
71
  }
73
- }
72
+ }