more commands

This commit is contained in:
Chris Tate
2026-01-10 11:57:58 -06:00
parent 9d253da8bd
commit 774002a351
6 changed files with 1154 additions and 105 deletions
+154 -99
View File
@@ -1,6 +1,6 @@
# veb # veb
Headless browser automation CLI for agents and humans. Headless browser automation CLI for agents and humans. Near-complete Playwright parity.
## Installation ## Installation
@@ -13,70 +13,99 @@ pnpm build
## Usage ## Usage
```bash ```bash
# Open a URL (auto-starts browser daemon) # Navigation
veb open https://example.com veb open https://example.com
# Click elements # Clicking
veb click "#submit-btn" veb click "#submit-btn"
veb click "text=Sign In" veb dblclick "#item"
# Type into inputs # Form input
veb type "#email" hello@example.com veb type "#search" "query" # Type character by character
veb type "#search" "search query" veb fill "#email" "test@example.com" # Clear and fill (faster)
veb check "#agree" # Check checkbox/radio
veb uncheck "#newsletter" # Uncheck checkbox
veb select "#country" "US" # Select dropdown
# Press keyboard keys # Keyboard
veb press Enter veb press Enter
veb press Tab veb press Tab
# Wait for things # Mouse
veb wait "#loading" # wait for selector veb hover "#menu"
veb wait --text "Welcome" # wait for text veb focus "#input"
veb wait 2000 # wait 2 seconds veb drag "#source" "#target"
# Take screenshots # File upload
veb upload "#file-input" ./document.pdf
veb upload "#files" ./a.png ./b.png
# Waiting
veb wait "#loading" # Wait for selector
veb wait --text "Welcome" # Wait for text
veb wait 2000 # Wait 2 seconds
# Screenshots & PDF
veb screenshot page.png veb screenshot page.png
veb screenshot --full page.png # full page veb screenshot --full page.png # Full page
veb screenshot -s "#hero" # specific element veb screenshot -s "#hero" # Specific element
veb pdf report.pdf
# Get accessibility snapshot (great for AI agents) # Content extraction
veb snapshot veb snapshot # Accessibility tree (best for agents)
veb extract "#main" # Get HTML
veb eval "document.title" # Run JavaScript
# Extract HTML content # Scrolling
veb extract "table"
veb extract "#main"
# Evaluate JavaScript
veb eval "document.title"
veb eval "window.location.href"
# Scroll the page
veb scroll down 500 veb scroll down 500
veb scroll up veb scroll up
veb scroll -s "#container" down veb scroll -s "#container" down
# Interact with dropdowns # Semantic locators (Playwright's recommended approach)
veb select "#country" "US" veb role button click --name "Submit"
veb role textbox fill "hello" --name "Email"
veb text "Sign In" click
veb text "Submit" click --exact
veb label "Email" fill "test@test.com"
veb placeholder "Search..." fill "query"
# Hover over elements # Frames/iframes
veb hover "#menu" veb frame "#iframe" # Switch to iframe
veb mainframe # Switch back to main
# Tab management # Cookies
veb tab new # Open new tab veb cookies # Get all cookies
veb tab list # List all tabs veb cookies set '[{"name":"session","value":"abc123","domain":".example.com"}]'
veb tab 0 # Switch to tab 0 veb cookies clear
veb tab close # Close current tab
veb tab close 1 # Close tab 1
# Window management # Storage
veb window new # Open new window veb storage local # Get all localStorage
veb storage local myKey # Get specific key
veb storage local set key value # Set value
veb storage local clear # Clear localStorage
veb storage session # sessionStorage (same commands)
# Session management (isolate multiple agents) # Dialogs (alerts, confirms, prompts)
veb dialog accept # Accept next dialog
veb dialog accept "input text" # Accept prompt with text
veb dialog dismiss # Dismiss next dialog
# Tabs
veb tab new
veb tab list
veb tab 0 # Switch to tab
veb tab close
# Windows
veb window new
# Sessions (isolate multiple agents)
veb --session agent1 open example.com veb --session agent1 open example.com
veb --session agent2 open google.com veb --session agent2 open google.com
veb session list # List active sessions VEB_SESSION=agent1 veb click "#btn"
VEB_SESSION=agent1 veb eval "document.title" veb session list
# Close browser (stops daemon) # Close browser
veb close veb close
``` ```
@@ -85,85 +114,111 @@ veb close
Use `--json` flag for machine-readable output: Use `--json` flag for machine-readable output:
```bash ```bash
veb open https://example.com --json
# {"id":"abc123","success":true,"data":{"url":"https://example.com/","title":"Example Domain"}}
veb snapshot --json veb snapshot --json
# {"id":"def456","success":true,"data":{"snapshot":"..."}} veb eval "document.title" --json
``` ```
## How It Works
veb runs a background daemon that keeps the browser open between commands. The first command automatically starts the daemon. Use `veb close` to shut it down.
## Commands Reference ## Commands Reference
### Navigation & Interaction
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `open <url>` | Navigate to a URL | | `open <url>` | Navigate to URL |
| `click <selector>` | Click an element | | `click <selector>` | Click element |
| `type <selector> <text>` | Type text into an element | | `dblclick <selector>` | Double-click |
| `press <key>` | Press a keyboard key | | `type <selector> <text>` | Type text |
| `wait <selector\|text\|ms>` | Wait for condition | | `fill <selector> <value>` | Clear & fill |
| `screenshot [path]` | Take a screenshot | | `press <key>` | Press key |
| `snapshot` | Get accessibility tree | | `check <selector>` | Check checkbox |
| `extract <selector>` | Get element HTML | | `uncheck <selector>` | Uncheck |
| `select <selector> <value>` | Select option |
| `hover <selector>` | Hover |
| `focus <selector>` | Focus |
| `drag <src> <target>` | Drag & drop |
| `upload <selector> <files>` | Upload files |
| `scroll <dir> [amount]` | Scroll |
### Semantic Locators
| Command | Description |
|---------|-------------|
| `role <role> <action>` | By ARIA role |
| `text <text> <action>` | By text |
| `label <label> <action>` | By label |
| `placeholder <ph> <action>` | By placeholder |
### Content & Screenshots
| Command | Description |
|---------|-------------|
| `screenshot [path]` | Screenshot |
| `pdf <path>` | Save as PDF |
| `snapshot` | Accessibility tree |
| `extract <selector>` | Get HTML |
| `eval <script>` | Run JavaScript | | `eval <script>` | Run JavaScript |
| `scroll <dir> [amount]` | Scroll page |
| `hover <selector>` | Hover over element | ### Browser State
| `select <selector> <val>` | Select dropdown option | | Command | Description |
| `tab new` | Open new tab | |---------|-------------|
| `tab list` | List all tabs | | `cookies` | Get cookies |
| `tab <index>` | Switch to tab | | `cookies set <json>` | Set cookies |
| `cookies clear` | Clear cookies |
| `storage local [key]` | Get localStorage |
| `storage local set <k> <v>` | Set localStorage |
| `storage local clear` | Clear localStorage |
| `dialog accept [text]` | Accept dialog |
| `dialog dismiss` | Dismiss dialog |
### Frames & Tabs
| Command | Description |
|---------|-------------|
| `frame <selector>` | Switch to frame |
| `mainframe` | Back to main |
| `tab new` | New tab |
| `tab list` | List tabs |
| `tab <index>` | Switch tab |
| `tab close [index]` | Close tab | | `tab close [index]` | Close tab |
| `window new` | Open new window | | `window new` | New window |
| `session` | Show current session |
| `session list` | List active sessions | ### Session & Control
| Command | Description |
|---------|-------------|
| `wait <sel\|text\|ms>` | Wait for condition |
| `session` | Show session |
| `session list` | List sessions |
| `close` | Close browser | | `close` | Close browser |
## Sessions
Sessions allow multiple agents to use veb simultaneously without interfering with each other. Each session runs its own isolated browser instance.
```bash
# Using --session flag
veb --session agent1 open https://site-a.com
veb --session agent2 open https://site-b.com
# Using environment variable
export VEB_SESSION=agent1
veb open https://example.com
veb click "#button"
# List all running sessions
veb session list
# Close a specific session
veb --session agent1 close
```
Sessions are identified by name. If no session is specified, the "default" session is used.
## Options ## Options
| Option | Description | | Option | Description |
|--------|-------------| |--------|-------------|
| `--session <name>` | Use isolated browser session | | `--session <name>` | Use isolated session |
| `--json` | Output raw JSON | | `--json` | JSON output |
| `--full, -f` | Full page screenshot | | `--full, -f` | Full page screenshot |
| `--text, -t` | Wait for text |
| `--selector, -s` | Target element | | `--selector, -s` | Target element |
| `--debug` | Show debug timing info | | `--name, -n` | Locator name filter |
| `--help, -h` | Show help | | `--exact` | Exact text match |
| `--text, -t` | Wait for text |
| `--debug` | Debug output |
## Selectors ## Selectors
veb supports all Playwright selectors: veb supports all Playwright selectors:
- CSS: `#id`, `.class`, `div.container` ```bash
- Text: `text=Click me`, `"Click me"` # CSS
- XPath: `xpath=//button` veb click "#id"
- Role: `role=button[name="Submit"]` veb click ".class"
veb click "div.container > button"
# Text
veb click "text=Click me"
# XPath
veb click "xpath=//button[@type='submit']"
# Semantic (recommended)
veb role button click --name "Submit"
veb label "Email" fill "test@test.com"
```
## License ## License
+326 -1
View File
@@ -1,4 +1,4 @@
import type { Page } from 'playwright'; import type { Page, Frame } from 'playwright';
import type { BrowserManager } from './browser.js'; import type { BrowserManager } from './browser.js';
import type { import type {
Command, Command,
@@ -6,6 +6,18 @@ import type {
NavigateCommand, NavigateCommand,
ClickCommand, ClickCommand,
TypeCommand, TypeCommand,
FillCommand,
CheckCommand,
UncheckCommand,
UploadCommand,
DoubleClickCommand,
FocusCommand,
DragCommand,
FrameCommand,
GetByRoleCommand,
GetByTextCommand,
GetByLabelCommand,
GetByPlaceholderCommand,
PressCommand, PressCommand,
ScreenshotCommand, ScreenshotCommand,
EvaluateCommand, EvaluateCommand,
@@ -17,6 +29,12 @@ import type {
TabSwitchCommand, TabSwitchCommand,
TabCloseCommand, TabCloseCommand,
WindowNewCommand, WindowNewCommand,
CookiesSetCommand,
StorageGetCommand,
StorageSetCommand,
StorageClearCommand,
DialogCommand,
PdfCommand,
NavigateData, NavigateData,
ScreenshotData, ScreenshotData,
EvaluateData, EvaluateData,
@@ -50,6 +68,32 @@ export async function executeCommand(
return await handleClick(command, browser); return await handleClick(command, browser);
case 'type': case 'type':
return await handleType(command, browser); return await handleType(command, browser);
case 'fill':
return await handleFill(command, browser);
case 'check':
return await handleCheck(command, browser);
case 'uncheck':
return await handleUncheck(command, browser);
case 'upload':
return await handleUpload(command, browser);
case 'dblclick':
return await handleDoubleClick(command, browser);
case 'focus':
return await handleFocus(command, browser);
case 'drag':
return await handleDrag(command, browser);
case 'frame':
return await handleFrame(command, browser);
case 'mainframe':
return await handleMainFrame(command, browser);
case 'getbyrole':
return await handleGetByRole(command, browser);
case 'getbytext':
return await handleGetByText(command, browser);
case 'getbylabel':
return await handleGetByLabel(command, browser);
case 'getbyplaceholder':
return await handleGetByPlaceholder(command, browser);
case 'press': case 'press':
return await handlePress(command, browser); return await handlePress(command, browser);
case 'screenshot': case 'screenshot':
@@ -80,6 +124,22 @@ export async function executeCommand(
return await handleTabClose(command, browser); return await handleTabClose(command, browser);
case 'window_new': case 'window_new':
return await handleWindowNew(command, browser); return await handleWindowNew(command, browser);
case 'cookies_get':
return await handleCookiesGet(command, browser);
case 'cookies_set':
return await handleCookiesSet(command, browser);
case 'cookies_clear':
return await handleCookiesClear(command, browser);
case 'storage_get':
return await handleStorageGet(command, browser);
case 'storage_set':
return await handleStorageSet(command, browser);
case 'storage_clear':
return await handleStorageClear(command, browser);
case 'dialog':
return await handleDialog(command, browser);
case 'pdf':
return await handlePdf(command, browser);
default: { default: {
// TypeScript narrows to never here, but we handle it for safety // TypeScript narrows to never here, but we handle it for safety
const unknownCommand = command as { id: string; action: string }; const unknownCommand = command as { id: string; action: string };
@@ -372,3 +432,268 @@ async function handleWindowNew(
const result = await browser.newWindow(command.viewport); const result = await browser.newWindow(command.viewport);
return successResponse(command.id, result); return successResponse(command.id, result);
} }
// New handlers for enhanced Playwright parity
async function handleFill(
command: FillCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
await frame.fill(command.selector, command.value);
return successResponse(command.id, { filled: true });
}
async function handleCheck(
command: CheckCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
await frame.check(command.selector);
return successResponse(command.id, { checked: true });
}
async function handleUncheck(
command: UncheckCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
await frame.uncheck(command.selector);
return successResponse(command.id, { unchecked: true });
}
async function handleUpload(
command: UploadCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
const files = Array.isArray(command.files) ? command.files : [command.files];
await frame.setInputFiles(command.selector, files);
return successResponse(command.id, { uploaded: files });
}
async function handleDoubleClick(
command: DoubleClickCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
await frame.dblclick(command.selector);
return successResponse(command.id, { clicked: true });
}
async function handleFocus(
command: FocusCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
await frame.focus(command.selector);
return successResponse(command.id, { focused: true });
}
async function handleDrag(
command: DragCommand,
browser: BrowserManager
): Promise<Response> {
const frame = browser.getFrame();
await frame.dragAndDrop(command.source, command.target);
return successResponse(command.id, { dragged: true });
}
async function handleFrame(
command: FrameCommand,
browser: BrowserManager
): Promise<Response> {
await browser.switchToFrame({
selector: command.selector,
name: command.name,
url: command.url,
});
return successResponse(command.id, { switched: true });
}
async function handleMainFrame(
command: Command & { action: 'mainframe' },
browser: BrowserManager
): Promise<Response> {
browser.switchToMainFrame();
return successResponse(command.id, { switched: true });
}
async function handleGetByRole(
command: GetByRoleCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByRole(command.role as any, { name: command.name });
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'fill':
await locator.fill(command.value ?? '');
return successResponse(command.id, { filled: true });
case 'check':
await locator.check();
return successResponse(command.id, { checked: true });
case 'hover':
await locator.hover();
return successResponse(command.id, { hovered: true });
}
}
async function handleGetByText(
command: GetByTextCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByText(command.text, { exact: command.exact });
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'hover':
await locator.hover();
return successResponse(command.id, { hovered: true });
}
}
async function handleGetByLabel(
command: GetByLabelCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByLabel(command.label);
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'fill':
await locator.fill(command.value ?? '');
return successResponse(command.id, { filled: true });
case 'check':
await locator.check();
return successResponse(command.id, { checked: true });
}
}
async function handleGetByPlaceholder(
command: GetByPlaceholderCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByPlaceholder(command.placeholder);
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'fill':
await locator.fill(command.value ?? '');
return successResponse(command.id, { filled: true });
}
}
async function handleCookiesGet(
command: Command & { action: 'cookies_get'; urls?: string[] },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const context = page.context();
const cookies = await context.cookies(command.urls);
return successResponse(command.id, { cookies });
}
async function handleCookiesSet(
command: CookiesSetCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const context = page.context();
await context.addCookies(command.cookies);
return successResponse(command.id, { set: true });
}
async function handleCookiesClear(
command: Command & { action: 'cookies_clear' },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const context = page.context();
await context.clearCookies();
return successResponse(command.id, { cleared: true });
}
async function handleStorageGet(
command: StorageGetCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
if (command.key) {
const value = await page.evaluate(
`${storageType}.getItem(${JSON.stringify(command.key)})`
);
return successResponse(command.id, { key: command.key, value });
} else {
const data = await page.evaluate(`
(() => {
const storage = ${storageType};
const result = {};
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key) result[key] = storage.getItem(key);
}
return result;
})()
`);
return successResponse(command.id, { data });
}
}
async function handleStorageSet(
command: StorageSetCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(
`${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})`
);
return successResponse(command.id, { set: true });
}
async function handleStorageClear(
command: StorageClearCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storageType}.clear()`);
return successResponse(command.id, { cleared: true });
}
async function handleDialog(
command: DialogCommand,
browser: BrowserManager
): Promise<Response> {
browser.setDialogHandler(command.response, command.promptText);
return successResponse(command.id, { handler: 'set', response: command.response });
}
async function handlePdf(
command: PdfCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.pdf({
path: command.path,
format: command.format ?? 'Letter',
});
return successResponse(command.id, { path: command.path });
}
+84 -1
View File
@@ -1,4 +1,4 @@
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright'; import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page, type Frame, type Dialog } from 'playwright';
import type { LaunchCommand } from './types.js'; import type { LaunchCommand } from './types.js';
/** /**
@@ -9,6 +9,8 @@ export class BrowserManager {
private contexts: BrowserContext[] = []; private contexts: BrowserContext[] = [];
private pages: Page[] = []; private pages: Page[] = [];
private activePageIndex: number = 0; private activePageIndex: number = 0;
private activeFrame: Frame | null = null;
private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
/** /**
* Check if browser is launched * Check if browser is launched
@@ -27,6 +29,87 @@ export class BrowserManager {
return this.pages[this.activePageIndex]; return this.pages[this.activePageIndex];
} }
/**
* Get the current frame (or page's main frame if no frame is selected)
*/
getFrame(): Frame {
if (this.activeFrame) {
return this.activeFrame;
}
return this.getPage().mainFrame();
}
/**
* Switch to a frame by selector, name, or URL
*/
async switchToFrame(options: { selector?: string; name?: string; url?: string }): Promise<void> {
const page = this.getPage();
if (options.selector) {
const frameElement = await page.$(options.selector);
if (!frameElement) {
throw new Error(`Frame not found: ${options.selector}`);
}
const frame = await frameElement.contentFrame();
if (!frame) {
throw new Error(`Element is not a frame: ${options.selector}`);
}
this.activeFrame = frame;
} else if (options.name) {
const frame = page.frame({ name: options.name });
if (!frame) {
throw new Error(`Frame not found with name: ${options.name}`);
}
this.activeFrame = frame;
} else if (options.url) {
const frame = page.frame({ url: options.url });
if (!frame) {
throw new Error(`Frame not found with URL: ${options.url}`);
}
this.activeFrame = frame;
}
}
/**
* Switch back to main frame
*/
switchToMainFrame(): void {
this.activeFrame = null;
}
/**
* Set up dialog handler
*/
setDialogHandler(response: 'accept' | 'dismiss', promptText?: string): void {
const page = this.getPage();
// Remove existing handler if any
if (this.dialogHandler) {
page.removeListener('dialog', this.dialogHandler);
}
this.dialogHandler = async (dialog: Dialog) => {
if (response === 'accept') {
await dialog.accept(promptText);
} else {
await dialog.dismiss();
}
};
page.on('dialog', this.dialogHandler);
}
/**
* Clear dialog handler
*/
clearDialogHandler(): void {
if (this.dialogHandler) {
const page = this.getPage();
page.removeListener('dialog', this.dialogHandler);
this.dialogHandler = null;
}
}
/** /**
* Get all pages * Get all pages
*/ */
+283 -3
View File
@@ -60,18 +60,53 @@ ${c('yellow', 'Usage:')}
${c('yellow', 'Commands:')} ${c('yellow', 'Commands:')}
${c('cyan', 'open')} <url> Open a URL in the browser ${c('cyan', 'open')} <url> Open a URL in the browser
${c('cyan', 'click')} <selector> Click an element ${c('cyan', 'click')} <selector> Click an element
${c('cyan', 'dblclick')} <selector> Double-click an element
${c('cyan', 'type')} <selector> <text> Type text into an element ${c('cyan', 'type')} <selector> <text> Type text into an element
${c('cyan', 'fill')} <selector> <value> Clear and fill input
${c('cyan', 'press')} <key> Press a keyboard key ${c('cyan', 'press')} <key> Press a keyboard key
${c('cyan', 'check')} <selector> Check a checkbox/radio
${c('cyan', 'uncheck')} <selector> Uncheck a checkbox
${c('cyan', 'select')} <selector> <value> Select dropdown option
${c('cyan', 'hover')} <selector> Hover over an element
${c('cyan', 'focus')} <selector> Focus an element
${c('cyan', 'drag')} <source> <target> Drag and drop
${c('cyan', 'upload')} <selector> <file...> Upload files
${c('cyan', 'wait')} <selector|text|ms> Wait for element, text, or duration ${c('cyan', 'wait')} <selector|text|ms> Wait for element, text, or duration
${c('cyan', 'screenshot')} [path] Take a screenshot ${c('cyan', 'screenshot')} [path] Take a screenshot
${c('cyan', 'pdf')} <path> Save page as PDF
${c('cyan', 'snapshot')} Get accessibility tree (for agents) ${c('cyan', 'snapshot')} Get accessibility tree (for agents)
${c('cyan', 'extract')} <selector> Extract element content ${c('cyan', 'extract')} <selector> Extract element content
${c('cyan', 'eval')} <script> Evaluate JavaScript ${c('cyan', 'eval')} <script> Evaluate JavaScript
${c('cyan', 'scroll')} <direction> [amount] Scroll the page ${c('cyan', 'scroll')} <direction> [amount] Scroll the page
${c('cyan', 'hover')} <selector> Hover over an element
${c('cyan', 'select')} <selector> <value> Select dropdown option
${c('cyan', 'close')} Close browser and stop daemon ${c('cyan', 'close')} Close browser and stop daemon
${c('yellow', 'Locator Commands:')}
${c('cyan', 'role')} <role> click|fill|check Find by ARIA role
${c('cyan', 'text')} <text> click|hover Find by text content
${c('cyan', 'label')} <label> click|fill Find by label
${c('cyan', 'placeholder')} <ph> click|fill Find by placeholder
${c('yellow', 'Frame Commands:')}
${c('cyan', 'frame')} <selector> Switch to iframe
${c('cyan', 'mainframe')} Switch back to main frame
${c('yellow', 'Cookie Commands:')}
${c('cyan', 'cookies')} Get all cookies
${c('cyan', 'cookies set')} <json> Set cookies
${c('cyan', 'cookies clear')} Clear all cookies
${c('yellow', 'Storage Commands:')}
${c('cyan', 'storage local')} [key] Get localStorage
${c('cyan', 'storage local set')} <k> <v> Set localStorage
${c('cyan', 'storage local clear')} Clear localStorage
${c('cyan', 'storage session')} [key] Get sessionStorage
${c('cyan', 'storage session set')} <k> <v> Set sessionStorage
${c('cyan', 'storage session clear')} Clear sessionStorage
${c('yellow', 'Dialog Commands:')}
${c('cyan', 'dialog accept')} [text] Accept next dialog
${c('cyan', 'dialog dismiss')} Dismiss next dialog
${c('yellow', 'Tab/Window Commands:')} ${c('yellow', 'Tab/Window Commands:')}
${c('cyan', 'tab new')} Open a new tab ${c('cyan', 'tab new')} Open a new tab
${c('cyan', 'tab list')} List all open tabs ${c('cyan', 'tab list')} List all open tabs
@@ -145,7 +180,37 @@ function printResponse(response: Response, jsonMode: boolean): void {
console.log(c('dim', ` ${(data.base64 as string).length} bytes`)); console.log(c('dim', ` ${(data.base64 as string).length} bytes`));
} else if (data.path) { } else if (data.path) {
console.log(c('green', '✓'), `Saved to ${data.path}`); console.log(c('green', '✓'), `Saved to ${data.path}`);
} else if (data.clicked || data.typed || data.pressed || data.hovered || data.scrolled || data.selected || data.waited) { } else if (data.cookies) {
// Cookies get
const cookies = data.cookies as Array<{ name: string; value: string; domain?: string }>;
if (cookies.length === 0) {
console.log(c('dim', 'No cookies'));
} else {
cookies.forEach(cookie => {
console.log(`${c('cyan', cookie.name)}: ${cookie.value}`);
if (cookie.domain) console.log(c('dim', ` domain: ${cookie.domain}`));
});
}
} else if (data.data) {
// Storage get (all)
const storage = data.data as Record<string, string>;
const keys = Object.keys(storage);
if (keys.length === 0) {
console.log(c('dim', 'Empty storage'));
} else {
keys.forEach(key => {
console.log(`${c('cyan', key)}: ${storage[key]}`);
});
}
} else if (data.value !== undefined && data.key) {
// Storage get (single key)
console.log(data.value ?? c('dim', 'null'));
} else if (data.uploaded) {
const files = data.uploaded as string[];
console.log(c('green', '✓'), `Uploaded ${files.length} file(s)`);
} else if (data.handler) {
console.log(c('green', '✓'), `Dialog handler set to ${data.response}`);
} else if (data.clicked || data.typed || data.pressed || data.hovered || data.scrolled || data.selected || data.waited || data.filled || data.checked || data.unchecked || data.focused || data.dragged || data.switched || data.set || data.cleared) {
console.log(c('green', '✓'), 'Done'); console.log(c('green', '✓'), 'Done');
} else if (data.launched) { } else if (data.launched) {
console.log(c('green', '✓'), 'Browser launched'); console.log(c('green', '✓'), 'Browser launched');
@@ -204,6 +269,7 @@ async function main(): Promise<void> {
const prev = args[i - 1]; const prev = args[i - 1];
if (prev === '--selector' || prev === '-s') return false; if (prev === '--selector' || prev === '-s') return false;
if (prev === '--session') return false; if (prev === '--session') return false;
if (prev === '--name' || prev === '-n') return false;
return true; return true;
}); });
const command = cleanArgs[0]; const command = cleanArgs[0];
@@ -215,6 +281,16 @@ async function main(): Promise<void> {
selectorOverride = args[sIdx + 1]; selectorOverride = args[sIdx + 1];
} }
// Find --name value (for locator commands)
let nameOverride: string | undefined;
const nIdx = args.findIndex(a => a === '--name' || a === '-n');
if (nIdx !== -1 && args[nIdx + 1]) {
nameOverride = args[nIdx + 1];
}
// Find --exact flag
const exactMode = args.includes('--exact');
const id = genId(); const id = genId();
let cmd: Record<string, unknown>; let cmd: Record<string, unknown>;
@@ -254,6 +330,80 @@ async function main(): Promise<void> {
break; break;
} }
case 'fill': {
const selector = cleanArgs[1];
const value = cleanArgs.slice(2).join(' ');
if (!selector || value === undefined) {
console.error(c('red', 'Error:'), 'Selector and value required');
process.exit(1);
}
cmd = { id, action: 'fill', selector, value };
break;
}
case 'check': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'check', selector };
break;
}
case 'uncheck': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'uncheck', selector };
break;
}
case 'dblclick':
case 'doubleclick': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'dblclick', selector };
break;
}
case 'focus': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'focus', selector };
break;
}
case 'drag': {
const source = cleanArgs[1];
const target = cleanArgs[2];
if (!source || !target) {
console.error(c('red', 'Error:'), 'Source and target selectors required');
process.exit(1);
}
cmd = { id, action: 'drag', source, target };
break;
}
case 'upload': {
const selector = cleanArgs[1];
const files = cleanArgs.slice(2);
if (!selector || files.length === 0) {
console.error(c('red', 'Error:'), 'Selector and file(s) required');
process.exit(1);
}
cmd = { id, action: 'upload', selector, files };
break;
}
case 'press': { case 'press': {
const key = cleanArgs[1]; const key = cleanArgs[1];
if (!key) { if (!key) {
@@ -357,6 +507,136 @@ async function main(): Promise<void> {
break; break;
} }
case 'pdf': {
const pdfPath = cleanArgs[1];
if (!pdfPath) {
console.error(c('red', 'Error:'), 'Path required');
process.exit(1);
}
cmd = { id, action: 'pdf', path: pdfPath };
break;
}
case 'frame': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Frame selector required');
process.exit(1);
}
cmd = { id, action: 'frame', selector };
break;
}
case 'mainframe': {
cmd = { id, action: 'mainframe' };
break;
}
case 'role': {
const role = cleanArgs[1];
const subaction = cleanArgs[2] as 'click' | 'fill' | 'check' | 'hover';
const value = cleanArgs[3];
if (!role || !subaction) {
console.error(c('red', 'Error:'), 'Role and action required (e.g., veb role button click --name "Submit")');
process.exit(1);
}
cmd = { id, action: 'getbyrole', role, name: nameOverride, subaction, value };
break;
}
case 'text': {
const text = cleanArgs[1];
const subaction = cleanArgs[2] as 'click' | 'hover';
if (!text || !subaction) {
console.error(c('red', 'Error:'), 'Text and action required (e.g., veb text "Submit" click)');
process.exit(1);
}
cmd = { id, action: 'getbytext', text, exact: exactMode, subaction };
break;
}
case 'label': {
const label = cleanArgs[1];
const subaction = cleanArgs[2] as 'click' | 'fill' | 'check';
const value = cleanArgs[3];
if (!label || !subaction) {
console.error(c('red', 'Error:'), 'Label and action required (e.g., veb label "Email" fill "test@test.com")');
process.exit(1);
}
cmd = { id, action: 'getbylabel', label, subaction, value };
break;
}
case 'placeholder': {
const placeholder = cleanArgs[1];
const subaction = cleanArgs[2] as 'click' | 'fill';
const value = cleanArgs[3];
if (!placeholder || !subaction) {
console.error(c('red', 'Error:'), 'Placeholder and action required');
process.exit(1);
}
cmd = { id, action: 'getbyplaceholder', placeholder, subaction, value };
break;
}
case 'cookies': {
const subCmd = cleanArgs[1];
if (subCmd === 'set') {
const jsonStr = cleanArgs.slice(2).join(' ');
try {
const cookies = JSON.parse(jsonStr);
cmd = { id, action: 'cookies_set', cookies: Array.isArray(cookies) ? cookies : [cookies] };
} catch {
console.error(c('red', 'Error:'), 'Invalid JSON for cookies');
process.exit(1);
}
} else if (subCmd === 'clear') {
cmd = { id, action: 'cookies_clear' };
} else {
cmd = { id, action: 'cookies_get' };
}
break;
}
case 'storage': {
const storageType = cleanArgs[1]; // 'local' or 'session'
const subCmd = cleanArgs[2];
if (storageType !== 'local' && storageType !== 'session') {
console.error(c('red', 'Error:'), 'Storage type must be "local" or "session"');
process.exit(1);
}
if (subCmd === 'set') {
const key = cleanArgs[3];
const value = cleanArgs.slice(4).join(' ');
if (!key) {
console.error(c('red', 'Error:'), 'Key required');
process.exit(1);
}
cmd = { id, action: 'storage_set', type: storageType, key, value };
} else if (subCmd === 'clear') {
cmd = { id, action: 'storage_clear', type: storageType };
} else {
// Get - subCmd might be a key or undefined
cmd = { id, action: 'storage_get', type: storageType, key: subCmd };
}
break;
}
case 'dialog': {
const response = cleanArgs[1];
const promptText = cleanArgs[2];
if (response !== 'accept' && response !== 'dismiss') {
console.error(c('red', 'Error:'), 'Dialog response must be "accept" or "dismiss"');
process.exit(1);
}
cmd = { id, action: 'dialog', response, promptText };
break;
}
case 'close': case 'close':
case 'quit': case 'quit':
case 'exit': { case 'exit': {
+153
View File
@@ -40,6 +40,138 @@ const typeSchema = baseCommandSchema.extend({
clear: z.boolean().optional(), clear: z.boolean().optional(),
}); });
const fillSchema = baseCommandSchema.extend({
action: z.literal('fill'),
selector: z.string().min(1),
value: z.string(),
});
const checkSchema = baseCommandSchema.extend({
action: z.literal('check'),
selector: z.string().min(1),
});
const uncheckSchema = baseCommandSchema.extend({
action: z.literal('uncheck'),
selector: z.string().min(1),
});
const uploadSchema = baseCommandSchema.extend({
action: z.literal('upload'),
selector: z.string().min(1),
files: z.union([z.string(), z.array(z.string())]),
});
const dblclickSchema = baseCommandSchema.extend({
action: z.literal('dblclick'),
selector: z.string().min(1),
});
const focusSchema = baseCommandSchema.extend({
action: z.literal('focus'),
selector: z.string().min(1),
});
const dragSchema = baseCommandSchema.extend({
action: z.literal('drag'),
source: z.string().min(1),
target: z.string().min(1),
});
const frameSchema = baseCommandSchema.extend({
action: z.literal('frame'),
selector: z.string().min(1).optional(),
name: z.string().optional(),
url: z.string().optional(),
});
const mainframeSchema = baseCommandSchema.extend({
action: z.literal('mainframe'),
});
const getByRoleSchema = baseCommandSchema.extend({
action: z.literal('getbyrole'),
role: z.string().min(1),
name: z.string().optional(),
subaction: z.enum(['click', 'fill', 'check', 'hover']),
value: z.string().optional(),
});
const getByTextSchema = baseCommandSchema.extend({
action: z.literal('getbytext'),
text: z.string().min(1),
exact: z.boolean().optional(),
subaction: z.enum(['click', 'hover']),
});
const getByLabelSchema = baseCommandSchema.extend({
action: z.literal('getbylabel'),
label: z.string().min(1),
subaction: z.enum(['click', 'fill', 'check']),
value: z.string().optional(),
});
const getByPlaceholderSchema = baseCommandSchema.extend({
action: z.literal('getbyplaceholder'),
placeholder: z.string().min(1),
subaction: z.enum(['click', 'fill']),
value: z.string().optional(),
});
const cookiesGetSchema = baseCommandSchema.extend({
action: z.literal('cookies_get'),
urls: z.array(z.string()).optional(),
});
const cookiesSetSchema = baseCommandSchema.extend({
action: z.literal('cookies_set'),
cookies: z.array(z.object({
name: z.string(),
value: z.string(),
url: z.string().optional(),
domain: z.string().optional(),
path: z.string().optional(),
expires: z.number().optional(),
httpOnly: z.boolean().optional(),
secure: z.boolean().optional(),
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
})),
});
const cookiesClearSchema = baseCommandSchema.extend({
action: z.literal('cookies_clear'),
});
const storageGetSchema = baseCommandSchema.extend({
action: z.literal('storage_get'),
key: z.string().optional(),
type: z.enum(['local', 'session']),
});
const storageSetSchema = baseCommandSchema.extend({
action: z.literal('storage_set'),
key: z.string().min(1),
value: z.string(),
type: z.enum(['local', 'session']),
});
const storageClearSchema = baseCommandSchema.extend({
action: z.literal('storage_clear'),
type: z.enum(['local', 'session']),
});
const dialogSchema = baseCommandSchema.extend({
action: z.literal('dialog'),
response: z.enum(['accept', 'dismiss']),
promptText: z.string().optional(),
});
const pdfSchema = baseCommandSchema.extend({
action: z.literal('pdf'),
path: z.string().min(1),
format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(),
});
const pressSchema = baseCommandSchema.extend({ const pressSchema = baseCommandSchema.extend({
action: z.literal('press'), action: z.literal('press'),
key: z.string().min(1), key: z.string().min(1),
@@ -134,6 +266,19 @@ const commandSchema = z.discriminatedUnion('action', [
navigateSchema, navigateSchema,
clickSchema, clickSchema,
typeSchema, typeSchema,
fillSchema,
checkSchema,
uncheckSchema,
uploadSchema,
dblclickSchema,
focusSchema,
dragSchema,
frameSchema,
mainframeSchema,
getByRoleSchema,
getByTextSchema,
getByLabelSchema,
getByPlaceholderSchema,
pressSchema, pressSchema,
screenshotSchema, screenshotSchema,
snapshotSchema, snapshotSchema,
@@ -149,6 +294,14 @@ const commandSchema = z.discriminatedUnion('action', [
tabSwitchSchema, tabSwitchSchema,
tabCloseSchema, tabCloseSchema,
windowNewSchema, windowNewSchema,
cookiesGetSchema,
cookiesSetSchema,
cookiesClearSchema,
storageGetSchema,
storageSetSchema,
storageClearSchema,
dialogSchema,
pdfSchema,
]); ]);
// Parse result type // Parse result type
+154 -1
View File
@@ -36,6 +36,138 @@ export interface TypeCommand extends BaseCommand {
clear?: boolean; clear?: boolean;
} }
export interface FillCommand extends BaseCommand {
action: 'fill';
selector: string;
value: string;
}
export interface CheckCommand extends BaseCommand {
action: 'check';
selector: string;
}
export interface UncheckCommand extends BaseCommand {
action: 'uncheck';
selector: string;
}
export interface UploadCommand extends BaseCommand {
action: 'upload';
selector: string;
files: string | string[];
}
export interface DoubleClickCommand extends BaseCommand {
action: 'dblclick';
selector: string;
}
export interface FocusCommand extends BaseCommand {
action: 'focus';
selector: string;
}
export interface DragCommand extends BaseCommand {
action: 'drag';
source: string;
target: string;
}
export interface FrameCommand extends BaseCommand {
action: 'frame';
selector?: string;
name?: string;
url?: string;
}
export interface MainFrameCommand extends BaseCommand {
action: 'mainframe';
}
export interface GetByRoleCommand extends BaseCommand {
action: 'getbyrole';
role: string;
name?: string;
subaction: 'click' | 'fill' | 'check' | 'hover';
value?: string;
}
export interface GetByTextCommand extends BaseCommand {
action: 'getbytext';
text: string;
exact?: boolean;
subaction: 'click' | 'hover';
}
export interface GetByLabelCommand extends BaseCommand {
action: 'getbylabel';
label: string;
subaction: 'click' | 'fill' | 'check';
value?: string;
}
export interface GetByPlaceholderCommand extends BaseCommand {
action: 'getbyplaceholder';
placeholder: string;
subaction: 'click' | 'fill';
value?: string;
}
export interface CookiesGetCommand extends BaseCommand {
action: 'cookies_get';
urls?: string[];
}
export interface CookiesSetCommand extends BaseCommand {
action: 'cookies_set';
cookies: Array<{
name: string;
value: string;
url?: string;
domain?: string;
path?: string;
expires?: number;
httpOnly?: boolean;
secure?: boolean;
sameSite?: 'Strict' | 'Lax' | 'None';
}>;
}
export interface CookiesClearCommand extends BaseCommand {
action: 'cookies_clear';
}
export interface StorageGetCommand extends BaseCommand {
action: 'storage_get';
key?: string;
type: 'local' | 'session';
}
export interface StorageSetCommand extends BaseCommand {
action: 'storage_set';
key: string;
value: string;
type: 'local' | 'session';
}
export interface StorageClearCommand extends BaseCommand {
action: 'storage_clear';
type: 'local' | 'session';
}
export interface DialogCommand extends BaseCommand {
action: 'dialog';
response: 'accept' | 'dismiss';
promptText?: string;
}
export interface PdfCommand extends BaseCommand {
action: 'pdf';
path: string;
format?: 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6';
}
export interface PressCommand extends BaseCommand { export interface PressCommand extends BaseCommand {
action: 'press'; action: 'press';
key: string; key: string;
@@ -127,6 +259,19 @@ export type Command =
| NavigateCommand | NavigateCommand
| ClickCommand | ClickCommand
| TypeCommand | TypeCommand
| FillCommand
| CheckCommand
| UncheckCommand
| UploadCommand
| DoubleClickCommand
| FocusCommand
| DragCommand
| FrameCommand
| MainFrameCommand
| GetByRoleCommand
| GetByTextCommand
| GetByLabelCommand
| GetByPlaceholderCommand
| PressCommand | PressCommand
| ScreenshotCommand | ScreenshotCommand
| SnapshotCommand | SnapshotCommand
@@ -141,7 +286,15 @@ export type Command =
| TabListCommand | TabListCommand
| TabSwitchCommand | TabSwitchCommand
| TabCloseCommand | TabCloseCommand
| WindowNewCommand; | WindowNewCommand
| CookiesGetCommand
| CookiesSetCommand
| CookiesClearCommand
| StorageGetCommand
| StorageSetCommand
| StorageClearCommand
| DialogCommand
| PdfCommand;
// Response types // Response types
export interface SuccessResponse<T = unknown> { export interface SuccessResponse<T = unknown> {