💬 Commit message: Update 2026-02-06 20:28:30, 13 files, 1401 lines
📁 Files changed: 13 📝 Lines changed: 1401 • .gitignore • CLAUDE.md • PLAN.md • README.md • TODO.md • package-lock.json • package.json • browser.ts • cli.ts • index.ts • server.ts • types.ts • tsconfig.json
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
/node_modules/
|
||||||
|
/public/
|
||||||
|
/dist/
|
||||||
|
/screenshots/
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build & Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npx playwright install webkit # Install WebKit browser
|
||||||
|
npm run build # Compile TypeScript to dist/
|
||||||
|
npm run dev # Watch mode compilation
|
||||||
|
npm start # Run the CLI (after build)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a TypeScript library providing headless browser automation via Playwright WebKit. It has three modes of operation:
|
||||||
|
|
||||||
|
1. **CLI** (`src/cli.ts`) - Direct command-line usage for screenshots, clicking, typing, and querying
|
||||||
|
2. **Server** (`src/server.ts`) - HTTP server accepting JSON commands via POST requests
|
||||||
|
3. **Library** (`src/index.ts`) - Programmatic API via `ClaudeBrowser` class
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
- **ClaudeBrowser** (`src/browser.ts`) - Main class wrapping Playwright WebKit. Handles browser lifecycle, navigation, and all DOM interactions. Uses `executeCommand()` to process typed command objects.
|
||||||
|
- **BrowserServer** (`src/server.ts`) - HTTP server that wraps ClaudeBrowser. Accepts JSON POST requests and returns JSON responses. CORS-enabled.
|
||||||
|
- **Types** (`src/types.ts`) - Discriminated union of command types (`BrowserCommand`) and response types (`CommandResponse`).
|
||||||
|
|
||||||
|
### Command Flow
|
||||||
|
|
||||||
|
Commands are typed objects with a `cmd` discriminator:
|
||||||
|
```typescript
|
||||||
|
{ cmd: 'goto', url: string }
|
||||||
|
{ cmd: 'click', selector: string }
|
||||||
|
{ cmd: 'type', selector: string, text: string }
|
||||||
|
{ cmd: 'query', selector: string }
|
||||||
|
// etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
All commands return `{ ok: true, ...data }` or `{ ok: false, error: string }`.
|
||||||
|
|
||||||
|
## CLI Options
|
||||||
|
|
||||||
|
Key flags: `-s <port>` (server mode), `-q <selector>` (query), `-c <selector>` (click), `-t <sel>=<text>` (type), `-i` (interactive), `--headed` (visible browser).
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# Plan: Web Debugging Commands
|
||||||
|
|
||||||
|
## Phase 1: Storage & Session Commands
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Add commands for inspecting and manipulating browser storage and cookies. These are essential for debugging authentication, session state, and client-side data persistence.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 1.1: Add Cookies Command
|
||||||
|
- **Objective**: Implement get/set/clear cookies functionality
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `CookiesCommand` type with `action: 'get' | 'set' | 'clear'`
|
||||||
|
- Add optional `name`, `value`, `url` fields for set operation
|
||||||
|
- Implement `cookies()` method using `context.cookies()` and `context.addCookies()`
|
||||||
|
- Add logging with cookie icon
|
||||||
|
|
||||||
|
#### Step 1.2: Add Storage Command
|
||||||
|
- **Objective**: Implement localStorage and sessionStorage read/write/clear
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: Step 1.1
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `StorageCommand` type with `storage: 'local' | 'session'` and `action: 'get' | 'set' | 'clear'`
|
||||||
|
- Implement via `page.evaluate()` accessing `window.localStorage` / `window.sessionStorage`
|
||||||
|
- Return all items on get, or specific key if provided
|
||||||
|
|
||||||
|
## Phase 2: Console & Debugging Commands
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Add commands for capturing console output and debugging page state. Critical for understanding client-side errors and application behavior.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 2.1: Add Console Command
|
||||||
|
- **Objective**: Capture and return console messages (log, error, warn, info)
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `ConsoleCommand` type with optional `clear: boolean`
|
||||||
|
- Store console messages in array via `page.on('console')`
|
||||||
|
- Initialize listener in `launch()` method
|
||||||
|
- Return accumulated messages with type, text, and timestamp
|
||||||
|
|
||||||
|
#### Step 2.2: Add Metrics Command
|
||||||
|
- **Objective**: Return performance metrics and DOM statistics
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `MetricsCommand` type
|
||||||
|
- Use `page.evaluate()` to gather `performance.timing`, DOM node counts
|
||||||
|
- Return structured metrics object
|
||||||
|
|
||||||
|
## Phase 3: Interaction Commands
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Add commands for advanced page interactions beyond click and type. Enables testing hover states, keyboard shortcuts, and form controls.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 3.1: Add Scroll Command
|
||||||
|
- **Objective**: Scroll to position, element, or by delta
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `ScrollCommand` with `selector?: string`, `x?: number`, `y?: number`, `behavior?: 'smooth' | 'instant'`
|
||||||
|
- If selector provided, use `element.scrollIntoView()`
|
||||||
|
- Otherwise use `window.scrollTo()`
|
||||||
|
|
||||||
|
#### Step 3.2: Add Hover Command
|
||||||
|
- **Objective**: Trigger hover state on element
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `HoverCommand` with `selector: string`
|
||||||
|
- Use `page.hover(selector)`
|
||||||
|
- Return success with element info
|
||||||
|
|
||||||
|
#### Step 3.3: Add Select Command
|
||||||
|
- **Objective**: Select option(s) in dropdown/select elements
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `SelectCommand` with `selector: string`, `value: string | string[]`
|
||||||
|
- Use `page.selectOption(selector, value)`
|
||||||
|
- Return selected values
|
||||||
|
|
||||||
|
#### Step 3.4: Add Keys Command
|
||||||
|
- **Objective**: Send keyboard shortcuts and special keys
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `KeysCommand` with `keys: string` (e.g., "Control+a", "Escape", "Enter")
|
||||||
|
- Use `page.keyboard.press()` for single keys
|
||||||
|
- Support key combinations
|
||||||
|
|
||||||
|
## Phase 4: Network Commands
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Add commands for network inspection and manipulation. Enables debugging API calls, blocking resources, and simulating network conditions.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 4.1: Add Network Logging
|
||||||
|
- **Objective**: Capture and return network requests/responses
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `NetworkCommand` with `action: 'log' | 'clear'`, optional `filter: string`
|
||||||
|
- Store requests via `page.on('request')` and `page.on('response')`
|
||||||
|
- Return array of {url, method, status, type, timing}
|
||||||
|
|
||||||
|
#### Step 4.2: Add Block Command
|
||||||
|
- **Objective**: Block URLs or patterns (ads, analytics, etc.)
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: Step 4.1
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `BlockCommand` with `patterns: string[]` and `action: 'add' | 'remove' | 'clear'`
|
||||||
|
- Use `page.route()` to abort matching requests
|
||||||
|
- Maintain list of blocked patterns
|
||||||
|
|
||||||
|
#### Step 4.3: Add Throttle Command
|
||||||
|
- **Objective**: Simulate slow network conditions
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `ThrottleCommand` with `preset: 'slow3g' | 'fast3g' | 'offline' | 'none'`
|
||||||
|
- Use CDP session to set network conditions via `Network.emulateNetworkConditions`
|
||||||
|
|
||||||
|
## Phase 5: Output Commands
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Add commands for exporting page content in different formats.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 5.1: Add PDF Command
|
||||||
|
- **Objective**: Save page as PDF
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `PdfCommand` with `path: string`, optional `format`, `landscape`, `margin`
|
||||||
|
- Use `page.pdf()` (WebKit supports this)
|
||||||
|
- Return path to saved file
|
||||||
|
|
||||||
|
#### Step 5.2: Add Accessibility Command
|
||||||
|
- **Objective**: Dump accessibility tree snapshot
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `AccessibilityCommand` with optional `selector` for subtree
|
||||||
|
- Use `page.accessibility.snapshot()`
|
||||||
|
- Return tree structure
|
||||||
|
|
||||||
|
## Phase 6: Advanced Commands
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Add commands for viewport manipulation, device emulation, and frame handling.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 6.1: Add Viewport Command
|
||||||
|
- **Objective**: Resize viewport dynamically
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `ViewportCommand` with `width: number`, `height: number`
|
||||||
|
- Use `page.setViewportSize()`
|
||||||
|
- Return new dimensions
|
||||||
|
|
||||||
|
#### Step 6.2: Add Emulate Command
|
||||||
|
- **Objective**: Emulate mobile devices
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: Step 6.1
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `EmulateCommand` with `device: string` (e.g., 'iPhone 12', 'Pixel 5')
|
||||||
|
- Use Playwright's device descriptors
|
||||||
|
- Apply viewport, userAgent, deviceScaleFactor
|
||||||
|
|
||||||
|
#### Step 6.3: Add Frames Command
|
||||||
|
- **Objective**: List and switch to iframe contexts
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `FramesCommand` with `action: 'list' | 'switch'`, optional `selector` or `index`
|
||||||
|
- Use `page.frames()` to list, `frame.locator()` to switch context
|
||||||
|
- Track current frame for subsequent commands
|
||||||
|
|
||||||
|
#### Step 6.4: Add Dialog Command
|
||||||
|
- **Objective**: Handle alert/confirm/prompt dialogs
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `DialogCommand` with `action: 'accept' | 'dismiss'`, optional `text` for prompt
|
||||||
|
- Set up `page.on('dialog')` handler
|
||||||
|
- Queue dialogs and respond based on command
|
||||||
|
|
||||||
|
#### Step 6.5: Add Upload Command
|
||||||
|
- **Objective**: Fill file input elements
|
||||||
|
- **Files**: `src/types.ts`, `src/browser.ts`, `src/server.ts`, `src/index.ts`
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Implementation**:
|
||||||
|
- Add `UploadCommand` with `selector: string`, `files: string[]`
|
||||||
|
- Use `page.setInputFiles(selector, files)`
|
||||||
|
- Return success with file count
|
||||||
|
|
||||||
|
## Phase 7: Documentation & Polish
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Update documentation and CLI help text with all new commands.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### Step 7.1: Update README
|
||||||
|
- **Objective**: Document all new commands with examples
|
||||||
|
- **Files**: `README.md`
|
||||||
|
- **Dependencies**: Phases 1-6
|
||||||
|
- **Implementation**:
|
||||||
|
- Add command reference section
|
||||||
|
- Include curl examples for each command
|
||||||
|
- Document response formats
|
||||||
|
|
||||||
|
#### Step 7.2: Update CLAUDE.md
|
||||||
|
- **Objective**: Update developer documentation
|
||||||
|
- **Files**: `CLAUDE.md`
|
||||||
|
- **Dependencies**: Step 7.1
|
||||||
|
- **Implementation**:
|
||||||
|
- Update architecture section with new command flow
|
||||||
|
- Document internal logging format
|
||||||
|
|
||||||
|
#### Step 7.3: Update CLI Help
|
||||||
|
- **Objective**: Update --help output with command list
|
||||||
|
- **Files**: `src/cli.ts`
|
||||||
|
- **Dependencies**: Phases 1-6
|
||||||
|
- **Implementation**:
|
||||||
|
- Expand server mode help section
|
||||||
|
- Group commands by category
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# @aladac/claude-browse
|
||||||
|
|
||||||
|
Headless browser automation for Claude Code using Playwright WebKit.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @aladac/claude-browse
|
||||||
|
npx playwright install webkit
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Take a screenshot
|
||||||
|
claude-browse https://example.com
|
||||||
|
|
||||||
|
# Custom viewport and output
|
||||||
|
claude-browse -o page.png -w 1920 -h 1080 https://example.com
|
||||||
|
|
||||||
|
# Query elements
|
||||||
|
claude-browse -q "a[href]" https://example.com
|
||||||
|
claude-browse -q "img" -j https://example.com # JSON output
|
||||||
|
|
||||||
|
# Click and interact
|
||||||
|
claude-browse -c "button.submit" https://example.com
|
||||||
|
claude-browse -t "input[name=q]=hello" -c "button[type=submit]" https://google.com
|
||||||
|
|
||||||
|
# Chain actions
|
||||||
|
claude-browse -c ".cookie-accept" -c "a.nav-link" -q "h1" https://example.com
|
||||||
|
|
||||||
|
# Interactive mode (visible browser)
|
||||||
|
claude-browse -i --headed https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Mode
|
||||||
|
|
||||||
|
Start a persistent browser server that accepts commands via HTTP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude-browse -s 3000 # headless
|
||||||
|
claude-browse -s 3000 --headed # visible browser
|
||||||
|
```
|
||||||
|
|
||||||
|
Send commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"goto","url":"https://example.com"}'
|
||||||
|
|
||||||
|
# Click
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"click","selector":"button.submit"}'
|
||||||
|
|
||||||
|
# Type
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"type","selector":"#search","text":"hello"}'
|
||||||
|
|
||||||
|
# Query elements
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"query","selector":"a[href]"}'
|
||||||
|
|
||||||
|
# Screenshot
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"screenshot","path":"page.png"}'
|
||||||
|
|
||||||
|
# Get current URL
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"url"}'
|
||||||
|
|
||||||
|
# Get page HTML
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"html"}'
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"back"}'
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"forward"}'
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"reload"}'
|
||||||
|
|
||||||
|
# Wait
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"wait","ms":2000}'
|
||||||
|
|
||||||
|
# Close server
|
||||||
|
curl -X POST localhost:3000 -d '{"cmd":"close"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Programmatic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ClaudeBrowser, startServer } from '@aladac/claude-browse';
|
||||||
|
|
||||||
|
// Direct browser control
|
||||||
|
const browser = new ClaudeBrowser({
|
||||||
|
headless: true,
|
||||||
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.launch();
|
||||||
|
await browser.goto('https://example.com');
|
||||||
|
|
||||||
|
const elements = await browser.query('a[href]');
|
||||||
|
console.log(elements);
|
||||||
|
|
||||||
|
await browser.click('button.submit');
|
||||||
|
await browser.type('#input', 'hello');
|
||||||
|
await browser.screenshot('page.png');
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Or start a server
|
||||||
|
const server = await startServer({ port: 3000, headless: false });
|
||||||
|
// Server runs until closed via HTTP or SIGINT
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### ClaudeBrowser
|
||||||
|
|
||||||
|
- `launch()` - Launch the browser
|
||||||
|
- `close()` - Close the browser
|
||||||
|
- `goto(url)` - Navigate to URL
|
||||||
|
- `click(selector)` - Click element
|
||||||
|
- `type(selector, text)` - Type into input
|
||||||
|
- `query(selector)` - Query elements, returns attributes
|
||||||
|
- `screenshot(path?, fullPage?)` - Take screenshot
|
||||||
|
- `getUrl()` - Get current URL and title
|
||||||
|
- `getHtml(full?)` - Get page HTML
|
||||||
|
- `back()` / `forward()` / `reload()` - Navigation
|
||||||
|
- `wait(ms)` - Wait for timeout
|
||||||
|
- `newPage()` - Open new page
|
||||||
|
- `executeCommand(cmd)` - Execute a command object
|
||||||
|
|
||||||
|
### BrowserServer
|
||||||
|
|
||||||
|
- `start()` - Start HTTP server
|
||||||
|
- `stop()` - Stop server and close browser
|
||||||
|
- `getPort()` - Get server port
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# TODO: Web Debugging Commands
|
||||||
|
|
||||||
|
## Phase 1: Storage & Session Commands
|
||||||
|
- [ ] Step 1.1: Add Cookies Command
|
||||||
|
- [ ] Step 1.2: Add Storage Command
|
||||||
|
|
||||||
|
## Phase 2: Console & Debugging Commands
|
||||||
|
- [ ] Step 2.1: Add Console Command
|
||||||
|
- [ ] Step 2.2: Add Metrics Command
|
||||||
|
|
||||||
|
## Phase 3: Interaction Commands
|
||||||
|
- [ ] Step 3.1: Add Scroll Command
|
||||||
|
- [ ] Step 3.2: Add Hover Command
|
||||||
|
- [ ] Step 3.3: Add Select Command
|
||||||
|
- [ ] Step 3.4: Add Keys Command
|
||||||
|
|
||||||
|
## Phase 4: Network Commands
|
||||||
|
- [ ] Step 4.1: Add Network Logging
|
||||||
|
- [ ] Step 4.2: Add Block Command
|
||||||
|
- [ ] Step 4.3: Add Throttle Command
|
||||||
|
|
||||||
|
## Phase 5: Output Commands
|
||||||
|
- [ ] Step 5.1: Add PDF Command
|
||||||
|
- [ ] Step 5.2: Add Accessibility Command
|
||||||
|
|
||||||
|
## Phase 6: Advanced Commands
|
||||||
|
- [ ] Step 6.1: Add Viewport Command
|
||||||
|
- [ ] Step 6.2: Add Emulate Command
|
||||||
|
- [ ] Step 6.3: Add Frames Command
|
||||||
|
- [ ] Step 6.4: Add Dialog Command
|
||||||
|
- [ ] Step 6.5: Add Upload Command
|
||||||
|
|
||||||
|
## Phase 7: Documentation & Polish
|
||||||
|
- [ ] Step 7.1: Update README
|
||||||
|
- [ ] Step 7.2: Update CLAUDE.md
|
||||||
|
- [ ] Step 7.3: Update CLI Help
|
||||||
Generated
+101
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "@aladac/claude-browse",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@aladac/claude-browse",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.41.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"claude-browse": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
|
||||||
|
"integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "@aladac/claude-browse",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Headless browser automation for Claude Code using Playwright WebKit",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"claude-browse": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/cli.js",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"playwright",
|
||||||
|
"webkit",
|
||||||
|
"browser",
|
||||||
|
"automation",
|
||||||
|
"claude",
|
||||||
|
"headless"
|
||||||
|
],
|
||||||
|
"author": "aladac",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/aladac/claude-browse"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.41.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
import { webkit, Browser, BrowserContext, Page } from 'playwright';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import type {
|
||||||
|
BrowserOptions,
|
||||||
|
BrowserCommand,
|
||||||
|
CommandResponse,
|
||||||
|
ElementInfo,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export class ClaudeBrowser {
|
||||||
|
private browser: Browser | null = null;
|
||||||
|
private context: BrowserContext | null = null;
|
||||||
|
private page: Page | null = null;
|
||||||
|
private options: Required<BrowserOptions>;
|
||||||
|
|
||||||
|
constructor(options: BrowserOptions = {}) {
|
||||||
|
this.options = {
|
||||||
|
headless: options.headless ?? true,
|
||||||
|
width: options.width ?? 1280,
|
||||||
|
height: options.height ?? 800,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async launch(): Promise<void> {
|
||||||
|
this.browser = await webkit.launch({ headless: this.options.headless });
|
||||||
|
this.context = await this.browser.newContext({
|
||||||
|
viewport: {
|
||||||
|
width: this.options.width,
|
||||||
|
height: this.options.height,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.page = await this.context.newPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this.browser) {
|
||||||
|
await this.browser.close();
|
||||||
|
this.browser = null;
|
||||||
|
this.context = null;
|
||||||
|
this.page = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensurePage(): Page {
|
||||||
|
if (!this.page) {
|
||||||
|
throw new Error('Browser not launched. Call launch() first.');
|
||||||
|
}
|
||||||
|
return this.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(url: string): Promise<{ url: string; title: string }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle' });
|
||||||
|
return { url: page.url(), title: await page.title() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async click(selector: string): Promise<{ url: string }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.click(selector);
|
||||||
|
await page.waitForLoadState('networkidle').catch(() => {});
|
||||||
|
return { url: page.url() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async type(selector: string, text: string): Promise<void> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.fill(selector, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(selector: string): Promise<ElementInfo[]> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
return page.$$eval(selector, (nodes) =>
|
||||||
|
nodes.map((el) => {
|
||||||
|
const attrs: Record<string, string> = {};
|
||||||
|
for (const attr of el.attributes) {
|
||||||
|
attrs[attr.name] = attr.value;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tag: el.tagName.toLowerCase(),
|
||||||
|
text: el.textContent?.trim().slice(0, 200) || '',
|
||||||
|
attributes: attrs,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshot(
|
||||||
|
path?: string,
|
||||||
|
fullPage = false
|
||||||
|
): Promise<{ path: string; buffer?: Buffer }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
const resolvedPath = resolve(path || 'screenshot.png');
|
||||||
|
const buffer = await page.screenshot({ path: resolvedPath, fullPage });
|
||||||
|
return { path: resolvedPath, buffer };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUrl(): Promise<{ url: string; title: string }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
return { url: page.url(), title: await page.title() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml(full = false): Promise<string> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
const html = await page.content();
|
||||||
|
return full ? html : html.slice(0, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async back(): Promise<{ url: string }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.goBack();
|
||||||
|
return { url: page.url() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async forward(): Promise<{ url: string }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.goForward();
|
||||||
|
return { url: page.url() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(): Promise<{ url: string }> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.reload();
|
||||||
|
return { url: page.url() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async wait(ms = 1000): Promise<void> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
await page.waitForTimeout(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
async newPage(): Promise<void> {
|
||||||
|
if (!this.context) {
|
||||||
|
throw new Error('Browser not launched. Call launch() first.');
|
||||||
|
}
|
||||||
|
this.page = await this.context.newPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async eval(script: string): Promise<unknown> {
|
||||||
|
const page = this.ensurePage();
|
||||||
|
return page.evaluate(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeCommand(cmd: BrowserCommand): Promise<CommandResponse> {
|
||||||
|
try {
|
||||||
|
switch (cmd.cmd) {
|
||||||
|
case 'goto': {
|
||||||
|
const result = await this.goto(cmd.url);
|
||||||
|
return { ok: true, ...result };
|
||||||
|
}
|
||||||
|
case 'click': {
|
||||||
|
const result = await this.click(cmd.selector);
|
||||||
|
return { ok: true, ...result };
|
||||||
|
}
|
||||||
|
case 'type': {
|
||||||
|
await this.type(cmd.selector, cmd.text);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
case 'query': {
|
||||||
|
const elements = await this.query(cmd.selector);
|
||||||
|
return { ok: true, count: elements.length, elements };
|
||||||
|
}
|
||||||
|
case 'screenshot': {
|
||||||
|
const result = await this.screenshot(cmd.path, cmd.fullPage);
|
||||||
|
return { ok: true, path: result.path };
|
||||||
|
}
|
||||||
|
case 'url': {
|
||||||
|
const result = await this.getUrl();
|
||||||
|
return { ok: true, ...result };
|
||||||
|
}
|
||||||
|
case 'html': {
|
||||||
|
const html = await this.getHtml(cmd.full);
|
||||||
|
return { ok: true, html };
|
||||||
|
}
|
||||||
|
case 'back': {
|
||||||
|
const result = await this.back();
|
||||||
|
return { ok: true, ...result };
|
||||||
|
}
|
||||||
|
case 'forward': {
|
||||||
|
const result = await this.forward();
|
||||||
|
return { ok: true, ...result };
|
||||||
|
}
|
||||||
|
case 'reload': {
|
||||||
|
const result = await this.reload();
|
||||||
|
return { ok: true, ...result };
|
||||||
|
}
|
||||||
|
case 'wait': {
|
||||||
|
await this.wait(cmd.ms);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
case 'newpage': {
|
||||||
|
await this.newPage();
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
case 'close': {
|
||||||
|
await this.close();
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
case 'eval': {
|
||||||
|
const result = await this.eval(cmd.script);
|
||||||
|
return { ok: true, result };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { ok: false, error: `Unknown command: ${(cmd as any).cmd}` };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { parseArgs } from 'node:util';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { ClaudeBrowser } from './browser.js';
|
||||||
|
import { startServer } from './server.js';
|
||||||
|
|
||||||
|
const { values, positionals } = parseArgs({
|
||||||
|
allowPositionals: true,
|
||||||
|
options: {
|
||||||
|
output: { type: 'string', short: 'o', default: 'screenshot.png' },
|
||||||
|
width: { type: 'string', short: 'w', default: '1280' },
|
||||||
|
height: { type: 'string', short: 'h', default: '800' },
|
||||||
|
fullpage: { type: 'boolean', short: 'f', default: false },
|
||||||
|
wait: { type: 'string', default: '2000' },
|
||||||
|
headed: { type: 'boolean', default: false },
|
||||||
|
interactive: { type: 'boolean', short: 'i', default: false },
|
||||||
|
query: { type: 'string', short: 'q' },
|
||||||
|
json: { type: 'boolean', short: 'j', default: false },
|
||||||
|
click: { type: 'string', short: 'c', multiple: true },
|
||||||
|
type: { type: 'string', short: 't', multiple: true },
|
||||||
|
serve: { type: 'string', short: 's' },
|
||||||
|
help: { type: 'boolean', default: false },
|
||||||
|
version: { type: 'boolean', short: 'v', default: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const HELP = `
|
||||||
|
Usage: claude-browse [options] <url>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --output <file> Output screenshot path (default: screenshot.png)
|
||||||
|
-w, --width <px> Viewport width (default: 1280)
|
||||||
|
-h, --height <px> Viewport height (default: 800)
|
||||||
|
-f, --fullpage Capture full page scroll
|
||||||
|
--wait <ms> Wait time after load (default: 2000)
|
||||||
|
--headed Show browser window
|
||||||
|
-i, --interactive Keep browser open for manual interaction
|
||||||
|
-q, --query <selector> Query elements by CSS selector and show attributes
|
||||||
|
-j, --json Output query results as JSON
|
||||||
|
-c, --click <selector> Click on element (can be repeated for multiple clicks)
|
||||||
|
-t, --type <sel>=<text> Type text into input (can be repeated)
|
||||||
|
-s, --serve <port> Start browser server on port (default: 3000)
|
||||||
|
-v, --version Show version
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
claude-browse https://example.com
|
||||||
|
claude-browse -o page.png -w 1920 -h 1080 https://example.com
|
||||||
|
claude-browse -i --headed https://example.com
|
||||||
|
claude-browse -q "a[href]" https://example.com
|
||||||
|
claude-browse -q "img" -j https://example.com
|
||||||
|
claude-browse -c "button.submit" https://example.com
|
||||||
|
claude-browse -t "input[name=q]=hello" -c "button[type=submit]" https://google.com
|
||||||
|
claude-browse -c ".cookie-accept" -c "a.nav-link" -q "h1" https://example.com
|
||||||
|
|
||||||
|
Server mode:
|
||||||
|
claude-browse -s 3000 # Start server on port 3000
|
||||||
|
claude-browse -s 3000 --headed # Start with visible browser
|
||||||
|
|
||||||
|
# Send commands via curl:
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"goto","url":"https://example.com"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"click","selector":"button"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"type","selector":"input","text":"hello"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"query","selector":"a[href]"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"screenshot","path":"shot.png"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"url"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"html"}'
|
||||||
|
curl -X POST http://localhost:3000 -d '{"cmd":"close"}'
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
if (values.version) {
|
||||||
|
console.log('claude-browse 0.1.0');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server mode
|
||||||
|
if (values.serve) {
|
||||||
|
const port = parseInt(values.serve) || 3000;
|
||||||
|
const server = await startServer({
|
||||||
|
port,
|
||||||
|
headless: !values.headed,
|
||||||
|
width: parseInt(values.width as string),
|
||||||
|
height: parseInt(values.height as string),
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('\nShutting down...');
|
||||||
|
await server.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep alive
|
||||||
|
await new Promise(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.help || positionals.length === 0) {
|
||||||
|
console.log(HELP);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = positionals[0];
|
||||||
|
const outputPath = resolve(values.output as string);
|
||||||
|
|
||||||
|
const browser = new ClaudeBrowser({
|
||||||
|
headless: !values.headed,
|
||||||
|
width: parseInt(values.width as string),
|
||||||
|
height: parseInt(values.height as string),
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.launch();
|
||||||
|
|
||||||
|
console.log(`Navigating to: ${url}`);
|
||||||
|
await browser.goto(url);
|
||||||
|
await browser.wait(parseInt(values.wait as string));
|
||||||
|
|
||||||
|
// Process type actions (before clicks, typically for form filling)
|
||||||
|
const typeActions = values.type as string[] | undefined;
|
||||||
|
if (typeActions?.length) {
|
||||||
|
for (const typeAction of typeActions) {
|
||||||
|
const eqIndex = typeAction.indexOf('=');
|
||||||
|
if (eqIndex === -1) {
|
||||||
|
console.error(
|
||||||
|
`Invalid --type format: "${typeAction}" (expected selector=text)`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const selector = typeAction.slice(0, eqIndex);
|
||||||
|
const text = typeAction.slice(eqIndex + 1);
|
||||||
|
console.log(`Typing "${text}" into: ${selector}`);
|
||||||
|
await browser.type(selector, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process click actions
|
||||||
|
const clickActions = values.click as string[] | undefined;
|
||||||
|
if (clickActions?.length) {
|
||||||
|
for (const selector of clickActions) {
|
||||||
|
console.log(`Clicking: ${selector}`);
|
||||||
|
await browser.click(selector);
|
||||||
|
await browser.wait(500);
|
||||||
|
}
|
||||||
|
const { url: currentUrl } = await browser.getUrl();
|
||||||
|
console.log(`Current URL: ${currentUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query elements if -q/--query is specified
|
||||||
|
if (values.query) {
|
||||||
|
const elements = await browser.query(values.query);
|
||||||
|
|
||||||
|
if (values.json) {
|
||||||
|
console.log(JSON.stringify(elements, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Found ${elements.length} element(s) matching "${values.query}":\n`
|
||||||
|
);
|
||||||
|
elements.forEach((el, i) => {
|
||||||
|
console.log(`[${i + 1}] <${el.tag}>`);
|
||||||
|
for (const [name, value] of Object.entries(el.attributes)) {
|
||||||
|
console.log(` ${name}="${value}"`);
|
||||||
|
}
|
||||||
|
if (el.text) {
|
||||||
|
console.log(
|
||||||
|
` text: "${el.text.slice(0, 100)}${el.text.length > 100 ? '...' : ''}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.interactive) {
|
||||||
|
console.log(`Saving screenshot to: ${outputPath}`);
|
||||||
|
await browser.screenshot(outputPath, values.fullpage);
|
||||||
|
await browser.close();
|
||||||
|
console.log('Done!');
|
||||||
|
} else {
|
||||||
|
console.log('Interactive mode - browser will stay open.');
|
||||||
|
console.log('Press Ctrl+C to exit.');
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('\nClosing browser...');
|
||||||
|
await browser.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep alive
|
||||||
|
await new Promise(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Error:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { ClaudeBrowser } from './browser.js';
|
||||||
|
export { BrowserServer, startServer } from './server.js';
|
||||||
|
export type {
|
||||||
|
BrowserOptions,
|
||||||
|
BrowserCommand,
|
||||||
|
CommandResponse,
|
||||||
|
ElementInfo,
|
||||||
|
SuccessResponse,
|
||||||
|
ErrorResponse,
|
||||||
|
GotoCommand,
|
||||||
|
ClickCommand,
|
||||||
|
TypeCommand,
|
||||||
|
QueryCommand,
|
||||||
|
ScreenshotCommand,
|
||||||
|
UrlCommand,
|
||||||
|
HtmlCommand,
|
||||||
|
BackCommand,
|
||||||
|
ForwardCommand,
|
||||||
|
ReloadCommand,
|
||||||
|
WaitCommand,
|
||||||
|
NewPageCommand,
|
||||||
|
CloseCommand,
|
||||||
|
EvalCommand,
|
||||||
|
} from './types.js';
|
||||||
|
export type { ServerOptions } from './server.js';
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
import { createServer, Server, IncomingMessage, ServerResponse } from 'node:http';
|
||||||
|
import { ClaudeBrowser } from './browser.js';
|
||||||
|
import type { BrowserCommand, BrowserOptions, CommandResponse } from './types.js';
|
||||||
|
|
||||||
|
export interface ServerOptions extends BrowserOptions {
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANSI colors
|
||||||
|
const c = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bold: '\x1b[1m',
|
||||||
|
dim: '\x1b[2m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
gray: '\x1b[90m',
|
||||||
|
white: '\x1b[37m',
|
||||||
|
bgBlue: '\x1b[44m',
|
||||||
|
bgGreen: '\x1b[42m',
|
||||||
|
bgYellow: '\x1b[43m',
|
||||||
|
bgMagenta: '\x1b[45m',
|
||||||
|
};
|
||||||
|
|
||||||
|
function timestamp(): string {
|
||||||
|
return c.gray + '[' + new Date().toISOString() + ']' + c.reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logCommand(cmd: BrowserCommand): void {
|
||||||
|
const ts = timestamp();
|
||||||
|
switch (cmd.cmd) {
|
||||||
|
case 'goto':
|
||||||
|
console.log(`${ts} ${c.cyan}${c.bold}→${c.reset} ${c.cyan}GOTO${c.reset} ${c.white}${cmd.url}${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
console.log(`${ts} ${c.yellow}${c.bold}◉${c.reset} ${c.yellow}CLICK${c.reset} ${c.white}${cmd.selector}${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'type':
|
||||||
|
console.log(`${ts} ${c.magenta}${c.bold}⌨${c.reset} ${c.magenta}TYPE${c.reset} ${c.white}${cmd.selector}${c.reset} ${c.dim}="${cmd.text}"${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'query':
|
||||||
|
console.log(`${ts} ${c.blue}${c.bold}?${c.reset} ${c.blue}QUERY${c.reset} ${c.white}${cmd.selector}${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'screenshot':
|
||||||
|
console.log(`${ts} ${c.green}${c.bold}📷${c.reset} ${c.green}SCREENSHOT${c.reset} ${c.dim}${cmd.path || 'screenshot.png'}${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'url':
|
||||||
|
console.log(`${ts} ${c.cyan}${c.bold}🔗${c.reset} ${c.cyan}URL${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'html':
|
||||||
|
console.log(`${ts} ${c.blue}${c.bold}<>${c.reset} ${c.blue}HTML${c.reset} ${cmd.full ? c.dim + '(full)' + c.reset : ''}`);
|
||||||
|
break;
|
||||||
|
case 'back':
|
||||||
|
console.log(`${ts} ${c.yellow}${c.bold}←${c.reset} ${c.yellow}BACK${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'forward':
|
||||||
|
console.log(`${ts} ${c.yellow}${c.bold}→${c.reset} ${c.yellow}FORWARD${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'reload':
|
||||||
|
console.log(`${ts} ${c.yellow}${c.bold}↻${c.reset} ${c.yellow}RELOAD${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'wait':
|
||||||
|
console.log(`${ts} ${c.gray}${c.bold}⏳${c.reset} ${c.gray}WAIT${c.reset} ${c.dim}${cmd.ms || 1000}ms${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'newpage':
|
||||||
|
console.log(`${ts} ${c.green}${c.bold}+${c.reset} ${c.green}NEW PAGE${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'close':
|
||||||
|
console.log(`${ts} ${c.red}${c.bold}✕${c.reset} ${c.red}CLOSE${c.reset}`);
|
||||||
|
break;
|
||||||
|
case 'eval':
|
||||||
|
const preview = cmd.script.length > 50 ? cmd.script.slice(0, 50) + '...' : cmd.script;
|
||||||
|
console.log(`${ts} ${c.magenta}${c.bold}⚡${c.reset} ${c.magenta}EVAL${c.reset} ${c.dim}${preview}${c.reset}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logResult(cmd: BrowserCommand, result: CommandResponse): void {
|
||||||
|
const ts = timestamp();
|
||||||
|
if (!result.ok) {
|
||||||
|
console.log(`${ts} ${c.red}✗ Error: ${result.error}${c.reset}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cmd.cmd) {
|
||||||
|
case 'goto':
|
||||||
|
if ('title' in result && result.title) {
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${result.title}${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
if ('url' in result) {
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}→ ${result.url}${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'query':
|
||||||
|
if ('count' in result) {
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}Found ${result.count} element(s)${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'screenshot':
|
||||||
|
if ('path' in result) {
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}Saved to ${result.path}${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'url':
|
||||||
|
if ('url' in result) {
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${result.url}${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'html':
|
||||||
|
if ('html' in result) {
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${result.html?.length || 0} chars${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'eval':
|
||||||
|
if ('result' in result) {
|
||||||
|
const json = JSON.stringify(result.result);
|
||||||
|
const preview = json && json.length > 80 ? json.slice(0, 80) + '...' : json;
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${preview}${c.reset}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`${ts} ${c.green}✓${c.reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BrowserServer {
|
||||||
|
private browser: ClaudeBrowser;
|
||||||
|
private server: Server | null = null;
|
||||||
|
private port: number;
|
||||||
|
|
||||||
|
constructor(options: ServerOptions = {}) {
|
||||||
|
this.browser = new ClaudeBrowser(options);
|
||||||
|
this.port = options.port ?? 3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
await this.browser.launch();
|
||||||
|
|
||||||
|
this.server = createServer(
|
||||||
|
async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
// CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: 'POST only' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = '';
|
||||||
|
for await (const chunk of req) {
|
||||||
|
body += chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cmd: BrowserCommand = JSON.parse(body);
|
||||||
|
logCommand(cmd);
|
||||||
|
|
||||||
|
// Handle close specially to shut down server
|
||||||
|
if (cmd.cmd === 'close') {
|
||||||
|
const result = { ok: true as const };
|
||||||
|
logResult(cmd, result);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
await this.stop();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.browser.executeCommand(cmd);
|
||||||
|
logResult(cmd, result);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} catch (err) {
|
||||||
|
const errorResult = { ok: false as const, error: (err as Error).message };
|
||||||
|
console.log(`${timestamp()} ${c.red}✗ ${errorResult.error}${c.reset}`);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(errorResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server!.listen(this.port, () => {
|
||||||
|
console.log();
|
||||||
|
console.log(`${c.bold}${c.cyan} 🌐 Claude Browse Server${c.reset}`);
|
||||||
|
console.log(`${c.dim} ─────────────────────────${c.reset}`);
|
||||||
|
console.log(` ${c.green}▶${c.reset} Listening on ${c.bold}http://localhost:${this.port}${c.reset}`);
|
||||||
|
console.log();
|
||||||
|
console.log(`${c.dim} Commands:${c.reset}`);
|
||||||
|
console.log(` ${c.cyan}goto${c.reset} ${c.yellow}click${c.reset} ${c.magenta}type${c.reset} ${c.blue}query${c.reset} ${c.green}screenshot${c.reset}`);
|
||||||
|
console.log(` ${c.cyan}url${c.reset} ${c.blue}html${c.reset} ${c.yellow}back${c.reset} ${c.yellow}forward${c.reset} ${c.yellow}reload${c.reset} ${c.gray}wait${c.reset} ${c.red}close${c.reset}`);
|
||||||
|
console.log();
|
||||||
|
console.log(`${c.dim} Example:${c.reset}`);
|
||||||
|
console.log(` ${c.gray}curl -X POST localhost:${this.port} -d '{"cmd":"goto","url":"https://example.com"}'${c.reset}`);
|
||||||
|
console.log();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
console.log(`\n${c.dim} Shutting down...${c.reset}`);
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close();
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
await this.browser.close();
|
||||||
|
console.log(` ${c.green}✓${c.reset} Browser closed\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPort(): number {
|
||||||
|
return this.port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startServer(options: ServerOptions = {}): Promise<BrowserServer> {
|
||||||
|
const server = new BrowserServer(options);
|
||||||
|
await server.start();
|
||||||
|
return server;
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
export interface BrowserOptions {
|
||||||
|
headless?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElementInfo {
|
||||||
|
tag: string;
|
||||||
|
text: string;
|
||||||
|
attributes: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command types
|
||||||
|
export interface GotoCommand {
|
||||||
|
cmd: 'goto';
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickCommand {
|
||||||
|
cmd: 'click';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypeCommand {
|
||||||
|
cmd: 'type';
|
||||||
|
selector: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryCommand {
|
||||||
|
cmd: 'query';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScreenshotCommand {
|
||||||
|
cmd: 'screenshot';
|
||||||
|
path?: string;
|
||||||
|
fullPage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UrlCommand {
|
||||||
|
cmd: 'url';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HtmlCommand {
|
||||||
|
cmd: 'html';
|
||||||
|
full?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackCommand {
|
||||||
|
cmd: 'back';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForwardCommand {
|
||||||
|
cmd: 'forward';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReloadCommand {
|
||||||
|
cmd: 'reload';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WaitCommand {
|
||||||
|
cmd: 'wait';
|
||||||
|
ms?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewPageCommand {
|
||||||
|
cmd: 'newpage';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CloseCommand {
|
||||||
|
cmd: 'close';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvalCommand {
|
||||||
|
cmd: 'eval';
|
||||||
|
script: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BrowserCommand =
|
||||||
|
| GotoCommand
|
||||||
|
| ClickCommand
|
||||||
|
| TypeCommand
|
||||||
|
| QueryCommand
|
||||||
|
| ScreenshotCommand
|
||||||
|
| UrlCommand
|
||||||
|
| HtmlCommand
|
||||||
|
| BackCommand
|
||||||
|
| ForwardCommand
|
||||||
|
| ReloadCommand
|
||||||
|
| WaitCommand
|
||||||
|
| NewPageCommand
|
||||||
|
| CloseCommand
|
||||||
|
| EvalCommand;
|
||||||
|
|
||||||
|
// Response types
|
||||||
|
export interface SuccessResponse {
|
||||||
|
ok: true;
|
||||||
|
url?: string;
|
||||||
|
title?: string;
|
||||||
|
path?: string;
|
||||||
|
html?: string;
|
||||||
|
count?: number;
|
||||||
|
elements?: ElementInfo[];
|
||||||
|
result?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandResponse = SuccessResponse | ErrorResponse;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user