Browser automation and inspection toolkit for AI agents. Debug web apps, inspect UI elements, automate workflows, take screenshots, click buttons, type text, and interact with web applications via WebSocket.
/plugin marketplace add stevengonsalvez/agent-bridge/plugin install debug-bridge@agent-bridge-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Enables AI agents to inspect and control web applications via WebSocket. Use this skill when you need to debug, test, automate, or interact with a running web application.
Use debug-bridge when the user asks you to:
┌─────────────┐ WebSocket ┌─────────────┐ WebSocket ┌─────────────┐
│ AI Agent │ ◄──────────────────► │ CLI Server │ ◄──────────────────►│ Browser │
│ (You/Claude)│ role=agent │ (Port 4000) │ role=app │ (Your App) │
└─────────────┘ └─────────────┘ └─────────────┘
# Using npx (recommended - no install needed)
npx debug-bridge-cli connect --session my-session
# Or with custom port
npx debug-bridge-cli connect --session my-session --port 4001
Expected output:
[DebugBridge] Server listening on ws://localhost:4000
[DebugBridge] Waiting for connections...
[DebugBridge] Session: my-session
The web app must have debug-bridge-browser SDK installed and configured.
Navigate to your app with query params:
http://localhost:5173?session=my-session&port=4000
Expected console output in browser:
[DebugBridge] Connecting to ws://localhost:4000/debug?role=app&sessionId=my-session
[DebugBridge] Connected
const sessionId = 'my-session';
const ws = new WebSocket(`ws://localhost:4000/debug?role=agent&sessionId=${sessionId}`);
ws.onopen = () => console.log('Connected to debug bridge');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('Received:', msg.type);
};
hello message with app info:{
"type": "hello",
"protocolVersion": 1,
"sessionId": "my-session",
"appName": "My App",
"url": "http://localhost:5173/",
"viewport": { "width": 1920, "height": 1080 }
}
capabilities message:{
"type": "capabilities",
"capabilities": ["dom_snapshot", "dom_mutations", "ui_tree", "console", "errors", "eval"]
}
console, error, state_update, dom_mutations messagesBefore sending commands, verify the system is ready:
async function healthCheck(ws, sessionId) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Health check timeout')), 5000);
let serverReady = false;
let browserConnected = false;
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'hello') {
browserConnected = true;
console.log('Browser connected:', msg.appName || msg.url);
}
if (msg.type === 'capabilities') {
serverReady = true;
console.log('Capabilities:', msg.capabilities);
}
if (serverReady && browserConnected) {
clearTimeout(timeout);
resolve({ serverReady, browserConnected, capabilities: msg.capabilities });
}
};
ws.onerror = (err) => {
clearTimeout(timeout);
reject(err);
};
});
}
All commands require base message fields:
const baseMessage = {
protocolVersion: 1,
sessionId: 'my-session',
timestamp: Date.now(),
requestId: crypto.randomUUID() // For correlating responses
};
Commands that target elements accept:
type ElementTarget = {
stableId?: string; // Preferred - stable across renders
selector?: string; // CSS selector fallback
text?: string; // Match by visible text
role?: string; // ARIA role
};
Priority: stableId > selector > text + role
| Command | Description | Parameters | Returns |
|---|---|---|---|
request_ui_tree | Get all interactive elements | - | UiTreeItem[] |
click | Click an element | target: ElementTarget | success: boolean |
type | Type text into input | target: ElementTarget, text: string, options?: {clear?, delay?, pressEnter?} | success: boolean |
hover | Hover over element | target: ElementTarget | success: boolean |
select | Select option in dropdown | target: ElementTarget, value?: string, label?: string, index?: number | success: boolean |
focus | Focus an element | target: ElementTarget | success: boolean |
scroll | Scroll page or element | target?: ElementTarget, x?: number, y?: number | success: boolean |
navigate | Navigate to URL | url: string | success: boolean |
evaluate | Execute JavaScript | code: string | result: any |
request_screenshot | Capture viewport | selector?: string, fullPage?: boolean | Base64 PNG |
request_state | Get cookies/localStorage | scope?: string | State object |
request_dom_snapshot | Get full HTML | - | html: string |
ws.send(JSON.stringify({
...baseMessage,
type: 'request_ui_tree'
}));
// Response: { type: 'ui_tree', items: [...] }
ws.send(JSON.stringify({
...baseMessage,
type: 'click',
target: { stableId: 'submit-btn-abc123' }
}));
// Response: { type: 'command_result', success: true, requestId: '...' }
ws.send(JSON.stringify({
...baseMessage,
type: 'type',
target: { selector: '#email-input' },
text: 'user@example.com',
options: {
clear: true, // Clear existing text first
delay: 50, // Delay between keystrokes (ms)
pressEnter: false // Press Enter after typing
}
}));
ws.send(JSON.stringify({
...baseMessage,
type: 'select',
target: { selector: '#country-select' },
label: 'United States' // Or: value: 'US', index: 5
}));
// Scroll by pixels
ws.send(JSON.stringify({
...baseMessage,
type: 'scroll',
y: 500 // Scroll down 500px
}));
// Scroll element into view
ws.send(JSON.stringify({
...baseMessage,
type: 'scroll',
target: { stableId: 'footer-element' }
}));
ws.send(JSON.stringify({
...baseMessage,
type: 'evaluate',
code: 'document.title'
}));
// Response: { type: 'command_result', result: 'My App Title' }
ws.send(JSON.stringify({
...baseMessage,
type: 'request_screenshot',
fullPage: true // Or selector: '#specific-element'
}));
// Response: { type: 'screenshot', data: 'base64...', width: 1920, height: 1080 }
| Code | Cause | Recovery Strategy |
|---|---|---|
TARGET_NOT_FOUND | Element not in DOM | Call request_ui_tree to refresh, verify selector |
TARGET_NOT_VISIBLE | Element off-screen or hidden | scroll to element first, wait for animation |
TARGET_DISABLED | Element is disabled | Check preconditions, wait for enable |
TIMEOUT | Operation exceeded time limit | Retry with backoff, check if page is loading |
EVAL_DISABLED | JavaScript eval blocked | Feature not available, use alternative |
EVAL_ERROR | JavaScript execution failed | Check code syntax, handle exceptions |
NAVIGATION_FAILED | URL navigation failed | Verify URL, check network |
INVALID_COMMAND | Malformed command | Check command structure against spec |
UNKNOWN_ERROR | Unexpected error | Log details, retry |
{
"type": "command_result",
"requestId": "abc123",
"requestType": "click",
"success": false,
"error": {
"code": "TARGET_NOT_FOUND",
"message": "Element with stableId 'btn-xyz' not found in DOM"
},
"duration": 150
}
async function clickWithRetry(ws, target, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const result = await sendCommand(ws, { type: 'click', target });
if (result.success) return result;
if (result.error?.code === 'TARGET_NOT_FOUND') {
// Element may have re-rendered, refresh UI tree
await sendCommand(ws, { type: 'request_ui_tree' });
await delay(500);
continue;
}
if (result.error?.code === 'TARGET_NOT_VISIBLE') {
// Scroll element into view
await sendCommand(ws, { type: 'scroll', target });
await delay(300);
continue;
}
throw new Error(`Click failed: ${result.error?.message}`);
}
throw new Error('Max retries exceeded');
}
async function loginFlow(ws, email, password) {
// 1. Get UI tree to discover elements
const uiTree = await requestUiTree(ws);
// 2. Find form elements
const emailInput = uiTree.find(i =>
i.role === 'textbox' && (i.meta.name?.includes('email') || i.meta.placeholder?.includes('email'))
);
const passwordInput = uiTree.find(i =>
i.role === 'textbox' && i.meta.type === 'password'
);
const submitBtn = uiTree.find(i =>
i.role === 'button' && (i.text?.toLowerCase().includes('sign') || i.text?.toLowerCase().includes('log'))
);
if (!emailInput || !passwordInput || !submitBtn) {
throw new Error('Login form elements not found');
}
// 3. Enter credentials
await type(ws, emailInput.stableId, email, { clear: true });
await type(ws, passwordInput.stableId, password, { clear: true });
// 4. Submit
await click(ws, submitBtn.stableId);
// 5. Wait for navigation/redirect
await waitForCondition(ws, async () => {
const state = await requestState(ws);
return state.localStorage?.authToken || state.cookies?.session;
}, 10000);
// 6. Verify success
const screenshot = await requestScreenshot(ws);
return { success: true, screenshot };
}
async function waitForElement(ws, predicate, timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const uiTree = await requestUiTree(ws);
const element = uiTree.find(predicate);
if (element) return element;
await delay(200);
}
throw new Error(`Element not found within ${timeout}ms`);
}
// Usage
const modal = await waitForElement(ws,
item => item.role === 'dialog' && item.visible
);
async function fillForm(ws, formData) {
const uiTree = await requestUiTree(ws);
for (const [fieldName, value] of Object.entries(formData)) {
const input = uiTree.find(i =>
i.meta.name === fieldName ||
i.meta.id === fieldName ||
i.label?.toLowerCase().includes(fieldName.toLowerCase())
);
if (!input) {
console.warn(`Field ${fieldName} not found`);
continue;
}
if (input.role === 'combobox' || input.meta.tagName === 'SELECT') {
await select(ws, input.stableId, { label: value });
} else if (input.role === 'checkbox') {
if (input.checked !== (value === true || value === 'true')) {
await click(ws, input.stableId);
}
} else {
await type(ws, input.stableId, value, { clear: true });
}
}
}
# Check if port is in use
lsof -i :4000
# Kill existing process
lsof -ti:4000 | xargs kill -9
# Try different port
npx debug-bridge-cli connect --session my-session --port 4001
?session=<id>&port=<port>[DebugBridge] logsload eventwaitForElementtext or selector instead of stableIdFor the web app to work with debug-bridge, add the SDK:
npm install debug-bridge-browser
// src/debug-bridge.ts
import { createDebugBridge } from 'debug-bridge-browser';
// Only in development
if (import.meta.env.DEV) {
const params = new URLSearchParams(window.location.search);
const sessionId = params.get('session');
const port = params.get('port') || '4000';
if (sessionId) {
const bridge = createDebugBridge({
url: `ws://localhost:${port}/debug?role=app&sessionId=${sessionId}`,
sessionId,
appName: 'My App',
});
bridge.connect();
// Expose for debugging
(window as any).__debugBridge = bridge;
}
}
# Via plugin (recommended)
claude plugin install github:stevengonsalvez/agent-bridge
# Or copy skill manually
cp -r skills/debug-bridge ~/.claude/skills/
cp -r skills/debug-bridge ~/.cursor/skills/
cp -r skills/debug-bridge ~/.codex/skills/
cp -r skills/debug-bridge .github/skills/
type UiTreeItem = {
stableId: string; // Stable identifier for targeting
selector: string; // CSS selector
role: string; // ARIA role (button, textbox, link, etc.)
text?: string; // Visible text content
label?: string; // aria-label or associated label
disabled: boolean; // Is element disabled
visible: boolean; // Is element visible
checked?: boolean; // For checkboxes/radios
value?: string; // Current value
meta: {
tagName: string; // HTML tag (BUTTON, INPUT, etc.)
type?: string; // Input type (text, password, etc.)
name?: string; // Input name attribute
href?: string; // Link href
placeholder?: string;
};
};
| Capability | Description |
|---|---|
dom_snapshot | Can request full HTML |
dom_mutations | Receives live DOM changes |
ui_tree | Can request interactive elements |
console | Receives console.log output |
errors | Receives JavaScript errors |
eval | Can execute JavaScript |
custom_state | App sends custom state |
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.