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
Empty file added C.patch
Empty file.
42 changes: 40 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ require('fs').writeFileSync('test.png', shot)
await vibe.quit()
```

With proxy:

```javascript
// HTTP proxy
const vibe = await browser.launch({
proxy: 'http://proxy.example.com:8080'
})

// SOCKS5 proxy with authentication
const vibe = await browser.launch({
proxy: 'socks5://user:pass@proxy.example.com:1080'
})
```

---

## Using the Python Client
Expand Down Expand Up @@ -203,6 +217,18 @@ async def main():
asyncio.run(main())
```

### Using a Proxy

Both sync and async APIs support proxy configuration:

```python
# Sync with HTTP proxy
vibe = browser_sync.launch(proxy='http://proxy.example.com:8080')

# Async with SOCKS5 proxy and authentication
vibe = await browser.launch(proxy='socks5://user:pass@proxy.example.com:1080')
```

---

## Using Clicker
Expand Down Expand Up @@ -251,12 +277,20 @@ After building, the binary is at `./clicker/bin/clicker`.
--headless # Hide the browser window (visible by default)
--wait-open 5 # Wait 5 seconds after navigation for page to load
--wait-close 3 # Keep browser open 3 seconds before closing
--proxy URL # Use a proxy server (HTTP, HTTPS, or SOCKS5)
```

Example:
Examples:

```bash
# Basic screenshot with wait
./clicker/bin/clicker screenshot https://example.com --wait-close 5 -o shot.png

# Using an HTTP proxy
./clicker/bin/clicker navigate https://example.com --proxy http://proxy.example.com:8080

# Using a SOCKS5 proxy with authentication
./clicker/bin/clicker navigate https://example.com --proxy socks5://user:pass@proxy.example.com:1080
```

---
Expand All @@ -269,14 +303,18 @@ Clicker includes an MCP (Model Context Protocol) server for AI agent integration

| Tool | Description |
|------|-------------|
| `browser_launch` | Start a browser session |
| `browser_launch` | Start a browser session (supports `headless` and `proxy` params) |
| `browser_navigate` | Go to a URL |
| `browser_click` | Click an element by CSS selector |
| `browser_type` | Type into an element |
| `browser_screenshot` | Capture the page |
| `browser_find` | Find element info |
| `browser_quit` | Close the browser |

The `browser_launch` tool accepts optional parameters:
- `headless` (boolean): Run browser in headless mode
- `proxy` (string): Proxy server URL (e.g., `http://proxy:8080`, `socks5://user:pass@proxy:1080`)

### Running the MCP Server

```bash
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ A single Go binary (~10MB) that does everything:
- **MCP Server:** stdio interface for LLM agents
- **Auto-Wait:** Polls for elements before interacting
- **Screenshots:** Viewport capture as PNG
- **Proxy Support:** HTTP, HTTPS, and SOCKS5 proxies with authentication

**Design goal:** The binary is invisible. JS developers just `npm install vibium` and it works.

Expand Down
22 changes: 12 additions & 10 deletions clicker/cmd/clicker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
waitOpen int
waitClose int
verbose bool
proxyFlag string
)

// doWaitOpen waits for page to load if --wait-open is set.
Expand Down Expand Up @@ -78,6 +79,7 @@ func main() {
rootCmd.PersistentFlags().IntVar(&waitOpen, "wait-open", 0, "Seconds to wait after navigation for page to load")
rootCmd.PersistentFlags().IntVar(&waitClose, "wait-close", 0, "Seconds to keep browser open before closing")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logging")
rootCmd.PersistentFlags().StringVar(&proxyFlag, "proxy", "", "Proxy server URL (e.g., http://proxy:8080, socks5://proxy:1080)")

rootCmd.AddCommand(&cobra.Command{
Use: "version",
Expand Down Expand Up @@ -135,7 +137,7 @@ func main() {
Use: "launch-test",
Short: "Launch browser via chromedriver and print BiDi WebSocket URL",
Run: func(cmd *cobra.Command, args []string) {
result, err := browser.Launch(browser.LaunchOptions{Headless: headless})
result, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -197,7 +199,7 @@ func main() {
Short: "Launch browser, connect via BiDi, send session.status",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("[1/5] Launching chromedriver...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: true, Verbose: true})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: true, Verbose: true, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -245,7 +247,7 @@ func main() {
url := args[0]

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -288,7 +290,7 @@ func main() {
output, _ := cmd.Flags().GetString("output")

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -353,7 +355,7 @@ func main() {
expression := args[1]

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -403,7 +405,7 @@ func main() {
selector := args[1]

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -459,7 +461,7 @@ func main() {
timeout, _ := cmd.Flags().GetDuration("timeout")

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -536,7 +538,7 @@ func main() {
timeout, _ := cmd.Flags().GetDuration("timeout")

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -609,7 +611,7 @@ func main() {
selector := args[1]

fmt.Println("Launching browser...")
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless, Proxy: proxyFlag})
if err != nil {
fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -671,7 +673,7 @@ func main() {
fmt.Printf("Starting Clicker proxy server on port %d...\n", port)

// Create router to manage browser sessions
router := proxy.NewRouter(headless)
router := proxy.NewRouter(headless, proxyFlag)

server := proxy.NewServer(
proxy.WithPort(port),
Expand Down
13 changes: 9 additions & 4 deletions clicker/internal/browser/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ func (pw *prefixWriter) Write(p []byte) (n int, err error) {
// LaunchOptions contains options for launching the browser.
type LaunchOptions struct {
Headless bool
Port int // Chromedriver port, 0 = auto-select
Verbose bool // Show chromedriver output
Port int // Chromedriver port, 0 = auto-select
Verbose bool // Show chromedriver output
Proxy string // Proxy server URL (e.g., http://proxy:8080, socks5://proxy:1080)
}

// LaunchResult contains the result of launching the browser via chromedriver.
Expand Down Expand Up @@ -145,7 +146,7 @@ func Launch(opts LaunchOptions) (*LaunchResult, error) {
}

// Create session with BiDi enabled
sessionID, wsURL, err := createSession(baseURL, chromePath, opts.Headless, opts.Verbose)
sessionID, wsURL, err := createSession(baseURL, chromePath, opts.Headless, opts.Proxy, opts.Verbose)
if err != nil {
cmd.Process.Kill()
return nil, fmt.Errorf("failed to create session: %w", err)
Expand Down Expand Up @@ -187,7 +188,7 @@ func waitForChromedriver(baseURL string, timeout time.Duration) error {
}

// createSession creates a new WebDriver session with BiDi enabled.
func createSession(baseURL, chromePath string, headless, verbose bool) (string, string, error) {
func createSession(baseURL, chromePath string, headless bool, proxy string, verbose bool) (string, string, error) {
args := []string{
"--no-first-run",
"--no-default-browser-check",
Expand Down Expand Up @@ -221,6 +222,10 @@ func createSession(baseURL, chromePath string, headless, verbose bool) (string,
args = append(args, "--headless=new")
}

if proxy != "" {
args = append(args, fmt.Sprintf("--proxy-server=%s", proxy))
}

reqBody := map[string]interface{}{
"capabilities": map[string]interface{}{
"alwaysMatch": map[string]interface{}{
Expand Down
17 changes: 15 additions & 2 deletions clicker/internal/mcp/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,16 @@ func (h *Handlers) browserLaunch(args map[string]interface{}) (*ToolsCallResult,
headless = val
}

proxy := ""
if val, ok := args["proxy"].(string); ok {
proxy = val
}

// Launch browser
launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless})
launchResult, err := browser.Launch(browser.LaunchOptions{
Headless: headless,
Proxy: proxy,
})
if err != nil {
return nil, fmt.Errorf("failed to launch browser: %w", err)
}
Expand All @@ -93,10 +101,15 @@ func (h *Handlers) browserLaunch(args map[string]interface{}) (*ToolsCallResult,
h.conn = conn
h.client = bidi.NewClient(conn)

msg := fmt.Sprintf("Browser launched (headless: %v)", headless)
if proxy != "" {
msg += fmt.Sprintf(", proxy: %s", proxy)
}

return &ToolsCallResult{
Content: []Content{{
Type: "text",
Text: fmt.Sprintf("Browser launched (headless: %v)", headless),
Text: msg,
}},
}, nil
}
Expand Down
4 changes: 4 additions & 0 deletions clicker/internal/mcp/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ func GetToolSchemas() []Tool {
"description": "Run browser in headless mode (no visible window)",
"default": false,
},
"proxy": map[string]interface{}{
"type": "string",
"description": "Proxy server URL (e.g., http://proxy:8080, socks5://proxy:1080, http://user:pass@proxy:8080)",
},
},
},
},
Expand Down
5 changes: 4 additions & 1 deletion clicker/internal/proxy/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ type bidiError struct {
type Router struct {
sessions sync.Map // map[uint64]*BrowserSession (client ID -> session)
headless bool
proxy string
}

// NewRouter creates a new router.
func NewRouter(headless bool) *Router {
func NewRouter(headless bool, proxy string) *Router {
return &Router{
headless: headless,
proxy: proxy,
}
}

Expand All @@ -70,6 +72,7 @@ func (r *Router) OnClientConnect(client *ClientConn) {
// Launch browser
launchResult, err := browser.Launch(browser.LaunchOptions{
Headless: r.headless,
Proxy: r.proxy,
})
if err != nil {
fmt.Printf("[router] Failed to launch browser for client %d: %v\n", client.ID, err)
Expand Down
6 changes: 4 additions & 2 deletions clients/javascript/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ export interface LaunchOptions {
headless?: boolean;
port?: number;
executablePath?: string;
proxy?: string;
}

export const browser = {
async launch(options: LaunchOptions = {}): Promise<Vibe> {
const { headless = false, port, executablePath } = options;
debug('launching browser', { headless, port, executablePath });
const { headless = false, port, executablePath, proxy } = options;
debug('launching browser', { headless, port, executablePath, proxy });

// Start the clicker process
const process = await ClickerProcess.start({
headless,
port,
executablePath,
proxy,
});
debug('clicker started', { port: process.port });

Expand Down
4 changes: 4 additions & 0 deletions clients/javascript/src/clicker/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ClickerProcessOptions {
port?: number;
headless?: boolean;
executablePath?: string;
proxy?: string;
}

export class ClickerProcess {
Expand Down Expand Up @@ -33,6 +34,9 @@ export class ClickerProcess {
if (options.headless === true) {
args.push('--headless');
}
if (options.proxy) {
args.push('--proxy', options.proxy);
}

const proc = spawn(binaryPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
Expand Down
1 change: 1 addition & 0 deletions clients/javascript/src/sync/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { VibeSync } from './vibe';

export interface LaunchOptions {
headless?: boolean;
proxy?: string;
}

export const browserSync = {
Expand Down
3 changes: 3 additions & 0 deletions clients/python/src/vibium/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ async def launch(
headless: bool = False,
port: Optional[int] = None,
executable_path: Optional[str] = None,
proxy: Optional[str] = None,
) -> Vibe:
"""Launch a new browser instance.

Args:
headless: Run browser in headless mode (default: visible).
port: WebSocket port (default: auto-assigned).
executable_path: Path to clicker binary (default: auto-detect).
proxy: Proxy server URL (e.g., http://proxy:8080, socks5://proxy:1080).

Returns:
A Vibe instance for browser automation.
Expand All @@ -36,6 +38,7 @@ async def launch(
headless=headless,
port=port,
executable_path=executable_path,
proxy=proxy,
)

client = await BiDiClient.connect(f"ws://localhost:{process.port}")
Expand Down
Loading