Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ bb login # Interactive provider selection
bb login anthropic # Login to Anthropic (OAuth)
bb login openai-codex # Login to OpenAI (OAuth)
bb login google # Login to Google (API key)
bb setup browser # Detect/configure Chrome/Chromium for browser_fetch
```

Or set environment variables: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, etc.
Expand All @@ -94,6 +95,7 @@ bb -r # Resume: pick from previous sessions
bb --model sonnet # Use a specific model
bb --model sonnet:high # Model with extended thinking
bb --list-models # List all available models
bb setup browser # Detect/configure Chrome/Chromium for browser_fetch
```

## Features
Expand Down
20 changes: 20 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod run;
mod session_bootstrap;
mod session_info;
mod session_navigation;
mod setup;
mod slash;
mod turn_runner;
mod update_check;
Expand All @@ -35,6 +36,7 @@ mod update_check;
bb --list-models sonnet Search models
bb login Login to a provider (OAuth)
bb logout Logout from a provider
bb setup browser Detect/configure Chrome/Chromium for browser_fetch
bb install npm:bb-example-skill Install a global package source
bb install --local ./my-skill Install a local/project package source
bb list List configured package sources
Expand Down Expand Up @@ -149,6 +151,11 @@ enum Commands {
/// Provider name
provider: Option<String>,
},
/// Setup optional local runtime dependencies
Setup {
#[command(subcommand)]
target: SetupCommands,
},
#[command(after_help = r#"Examples:
bb install npm:bb-example-skill
bb install --local npm:my-project-skill
Expand Down Expand Up @@ -202,6 +209,16 @@ Notes:
},
}

#[derive(Subcommand)]
enum SetupCommands {
/// Detect/configure a local browser for browser_fetch
Browser {
/// Persist BB_BROWSER to your shell rc file
#[arg(long)]
persist: bool,
},
}

#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
Expand Down Expand Up @@ -238,6 +255,9 @@ async fn main() -> Result<()> {
return match cmd {
Commands::Login { provider } => login::handle_login(provider.as_deref()).await,
Commands::Logout { provider } => login::handle_logout(provider.as_deref()).await,
Commands::Setup { target } => match target {
SetupCommands::Browser { persist } => setup::handle_setup_browser(*persist),
},
Commands::Install { local, source } => {
let scope = if *local {
extensions::SettingsScope::Project
Expand Down
208 changes: 208 additions & 0 deletions crates/cli/src/setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use anyhow::Result;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShellKind {
Bash,
Zsh,
Fish,
Sh,
}

pub fn handle_setup_browser(persist: bool) -> Result<()> {
match bb_tools::browser_fetch::resolve_browser_executable_path() {
Some(browser) => report_found_browser(&browser, persist),
None => {
println!("No browser_fetch-compatible browser was detected.\n");
println!("{}", bb_tools::browser_fetch::missing_browser_setup_message());
println!();
print_browser_install_guidance();
println!();
println!("After installing a browser, rerun:");
println!(" bb setup browser --persist");
Ok(())
}
}
}

fn report_found_browser(browser: &Path, persist: bool) -> Result<()> {
let shell_kind = detect_shell_kind(env::var("SHELL").ok().as_deref());
let export_line = browser_export_line(shell_kind, browser);

println!("Found browser_fetch-compatible browser:\n {}", browser.display());
println!();
println!("Use it in the current shell:");
println!(" {export_line}");

if persist {
let rc_path = detect_shell_rc_path(shell_kind)?;
let changed = persist_browser_export(&rc_path, shell_kind, browser)?;
println!();
if changed {
println!("Saved BB_BROWSER to {}", rc_path.display());
} else {
println!("BB_BROWSER was already configured in {}", rc_path.display());
}
println!("Open a new shell, or run:");
println!(" source {}", rc_path.display());
} else if let Ok(rc_path) = detect_shell_rc_path(shell_kind) {
println!();
println!("To persist it for future shells, run:");
println!(" bb setup browser --persist");
println!(" # writes BB_BROWSER to {}", rc_path.display());
}

Ok(())
}

fn print_browser_install_guidance() {
if cfg!(target_os = "linux") {
println!("Linux install hints:");
println!(" Ubuntu: sudo snap install chromium");
println!(" Then rerun: bb setup browser --persist");
} else if cfg!(target_os = "macos") {
println!("macOS install hints:");
println!(" Install Google Chrome or Chromium, then rerun: bb setup browser --persist");
} else if cfg!(target_os = "windows") {
println!("Windows install hints:");
println!(" Install Google Chrome / Microsoft Edge, then rerun: bb setup browser --persist");
} else {
println!("Install a Chrome/Chromium-compatible browser, then rerun: bb setup browser --persist");
}
}

fn detect_shell_kind(shell: Option<&str>) -> ShellKind {
match shell.unwrap_or_default() {
value if value.contains("fish") => ShellKind::Fish,
value if value.contains("zsh") => ShellKind::Zsh,
value if value.contains("bash") => ShellKind::Bash,
_ => ShellKind::Sh,
}
}

fn detect_shell_rc_path(shell_kind: ShellKind) -> Result<PathBuf> {
let home = env::var("HOME")?;
let home = PathBuf::from(home);
let path = match shell_kind {
ShellKind::Fish => home.join(".config/fish/config.fish"),
ShellKind::Zsh => home.join(".zshrc"),
ShellKind::Bash => home.join(".bashrc"),
ShellKind::Sh => home.join(".profile"),
};
Ok(path)
}

fn browser_export_line(shell_kind: ShellKind, browser: &Path) -> String {
let escaped = escape_double_quoted_path(browser);
match shell_kind {
ShellKind::Fish => format!("set -gx BB_BROWSER \"{escaped}\""),
_ => format!("export BB_BROWSER=\"{escaped}\""),
}
}

fn escape_double_quoted_path(path: &Path) -> String {
path.display()
.to_string()
.replace('\\', "\\\\")
.replace('"', "\\\"")
}

fn persist_browser_export(rc_path: &Path, shell_kind: ShellKind, browser: &Path) -> Result<bool> {
let export_line = browser_export_line(shell_kind, browser);
let existing = fs::read_to_string(rc_path).unwrap_or_default();
let updated = upsert_browser_export(&existing, shell_kind, &export_line);
if updated == existing {
return Ok(false);
}

if let Some(parent) = rc_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(rc_path, updated)?;
Ok(true)
}

fn upsert_browser_export(existing: &str, shell_kind: ShellKind, export_line: &str) -> String {
let mut replaced = false;
let mut lines = Vec::new();

for line in existing.lines() {
let trimmed = line.trim_start();
let is_browser_line = match shell_kind {
ShellKind::Fish => {
trimmed.starts_with("set -gx BB_BROWSER ")
|| trimmed.starts_with("set -x BB_BROWSER ")
}
_ => trimmed.starts_with("export BB_BROWSER="),
};

if is_browser_line {
if !replaced {
lines.push(export_line.to_string());
replaced = true;
}
continue;
}

lines.push(line.to_string());
}

if !replaced {
if !lines.is_empty() && !lines.last().is_some_and(|line| line.is_empty()) {
lines.push(String::new());
}
lines.push("# Added by bb setup browser".to_string());
lines.push(export_line.to_string());
}

let mut output = lines.join("\n");
if !output.is_empty() {
output.push('\n');
}
output
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn detects_shell_kind_from_shell_path() {
assert_eq!(detect_shell_kind(Some("/bin/zsh")), ShellKind::Zsh);
assert_eq!(detect_shell_kind(Some("/usr/bin/fish")), ShellKind::Fish);
assert_eq!(detect_shell_kind(Some("/bin/bash")), ShellKind::Bash);
assert_eq!(detect_shell_kind(Some("/bin/sh")), ShellKind::Sh);
}

#[test]
fn sh_export_is_inserted_with_marker() {
let updated = upsert_browser_export("alias ll='ls -l'\n", ShellKind::Bash, "export BB_BROWSER=\"/snap/bin/chromium\"");
assert!(updated.contains("# Added by bb setup browser"));
assert!(updated.contains("export BB_BROWSER=\"/snap/bin/chromium\""));
}

#[test]
fn existing_export_is_replaced_once() {
let updated = upsert_browser_export(
"export BB_BROWSER=\"/old/path\"\nexport PATH=\"/tmp:$PATH\"\n",
ShellKind::Zsh,
"export BB_BROWSER=\"/new/path\"",
);
assert!(updated.contains("export BB_BROWSER=\"/new/path\""));
assert!(!updated.contains("/old/path"));
}

#[test]
fn fish_export_uses_set_gx() {
let line = browser_export_line(
ShellKind::Fish,
Path::new("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
);
assert_eq!(
line,
"set -gx BB_BROWSER \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\""
);
}
}
9 changes: 9 additions & 0 deletions crates/tools/src/browser_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use async_trait::async_trait;
use bb_core::error::{BbError, BbResult};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::path::PathBuf;
use tokio_util::sync::CancellationToken;

use crate::support::{emit_progress_line, text_result};
Expand Down Expand Up @@ -34,6 +35,14 @@ pub struct BrowserFetchInput {

pub struct BrowserFetchTool;

pub fn resolve_browser_executable_path() -> Option<PathBuf> {
resolve_browser_executable()
}

pub fn missing_browser_setup_message() -> String {
missing_browser_error_message()
}

#[async_trait]
impl Tool for BrowserFetchTool {
fn name(&self) -> &str {
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,5 @@ Templates fully replace the default system prompt when used.
| `GROQ_API_KEY` | Groq API key |
| `XAI_API_KEY` | xAI API key |
| `OPENROUTER_API_KEY` | OpenRouter API key |
| `BB_BROWSER` | Path to Chrome/Chromium binary for `browser_fetch` |
| `BB_BROWSER` | Path to Chrome/Chromium binary for `browser_fetch` (or run `bb setup browser`) |
| `BB_TUI_COMPAT` | Enable ASCII-safe TUI compatibility mode |
2 changes: 1 addition & 1 deletion docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Fetch a page using a real headless Chrome/Chromium browser. Use when `web_fetch`
| `max_chars` | number | | Max characters (default: 20000) |
| `timeout` | number | | Timeout in seconds (default: 25) |

Requires Chrome or Chromium installed. Set `BB_BROWSER` env var to specify a custom binary path.
Requires Chrome or Chromium installed. Set `BB_BROWSER` env var to specify a custom binary path, or run `bb setup browser` to detect/configure a compatible local browser.

## Restricting Tools

Expand Down