agent-browser 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -0
- package/README.md +68 -11
- package/benchmark/benchmark.ts +521 -0
- package/benchmark/run.ts +322 -0
- package/bin/agent-browser +0 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +46 -35
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +31 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +62 -2
- package/dist/browser.js.map +1 -1
- package/dist/cli-light.d.ts +11 -0
- package/dist/cli-light.d.ts.map +1 -0
- package/dist/cli-light.js +409 -0
- package/dist/cli-light.js.map +1 -0
- package/dist/index.js +13 -8
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -1
- package/dist/snapshot.d.ts +63 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +298 -0
- package/dist/snapshot.js.map +1 -0
- package/package.json +5 -4
- package/src/actions.ts +50 -36
- package/src/browser.ts +73 -2
- package/src/cli-light.ts +457 -0
- package/src/index.ts +13 -8
- package/src/protocol.ts +4 -0
- package/src/snapshot.ts +380 -0
- package/tsconfig.json +12 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding agents working with this codebase.
|
|
4
|
+
|
|
5
|
+
<!-- opensrc:start -->
|
|
6
|
+
|
|
7
|
+
## Source Code Reference
|
|
8
|
+
|
|
9
|
+
Source code for dependencies is available in `opensrc/` for deeper understanding of implementation details.
|
|
10
|
+
|
|
11
|
+
See `opensrc/sources.json` for the list of available packages and their versions.
|
|
12
|
+
|
|
13
|
+
Use this source code when you need to understand how a package works internally, not just its types/interface.
|
|
14
|
+
|
|
15
|
+
### Fetching Additional Source Code
|
|
16
|
+
|
|
17
|
+
To fetch source code for a package or repository you need to understand, run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx opensrc <package> # npm package (e.g., npx opensrc zod)
|
|
21
|
+
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
|
|
22
|
+
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
|
|
23
|
+
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
<!-- opensrc:end -->
|
package/README.md
CHANGED
|
@@ -14,13 +14,22 @@ pnpm build
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
agent-browser open example.com
|
|
17
|
-
agent-browser
|
|
18
|
-
agent-browser
|
|
19
|
-
agent-browser
|
|
17
|
+
agent-browser snapshot # Get accessibility tree with refs
|
|
18
|
+
agent-browser click @e2 # Click by ref from snapshot
|
|
19
|
+
agent-browser fill @e3 "test@example.com" # Fill by ref
|
|
20
|
+
agent-browser get text @e1 # Get text by ref
|
|
20
21
|
agent-browser screenshot page.png
|
|
21
22
|
agent-browser close
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
### Traditional Selectors (also supported)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
agent-browser click "#submit"
|
|
29
|
+
agent-browser fill "#email" "test@example.com"
|
|
30
|
+
agent-browser find role button click --name "Submit"
|
|
31
|
+
```
|
|
32
|
+
|
|
24
33
|
## Commands
|
|
25
34
|
|
|
26
35
|
### Core Commands
|
|
@@ -48,7 +57,7 @@ agent-browser upload <sel> <files> # Upload files
|
|
|
48
57
|
agent-browser download [path] # Wait for download
|
|
49
58
|
agent-browser screenshot [path] # Take screenshot (--full for full page)
|
|
50
59
|
agent-browser pdf <path> # Save as PDF
|
|
51
|
-
agent-browser snapshot # Accessibility tree (best for AI)
|
|
60
|
+
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
|
52
61
|
agent-browser eval <js> # Run JavaScript
|
|
53
62
|
agent-browser close # Close browser
|
|
54
63
|
```
|
|
@@ -244,19 +253,49 @@ agent-browser session list
|
|
|
244
253
|
|
|
245
254
|
## Selectors
|
|
246
255
|
|
|
256
|
+
### Refs (Recommended for AI)
|
|
257
|
+
|
|
258
|
+
Refs provide deterministic element selection from snapshots:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# 1. Get snapshot with refs
|
|
262
|
+
agent-browser snapshot
|
|
263
|
+
# Output:
|
|
264
|
+
# - heading "Example Domain" [ref=e1] [level=1]
|
|
265
|
+
# - button "Submit" [ref=e2]
|
|
266
|
+
# - textbox "Email" [ref=e3]
|
|
267
|
+
# - link "Learn more" [ref=e4]
|
|
268
|
+
|
|
269
|
+
# 2. Use refs to interact
|
|
270
|
+
agent-browser click @e2 # Click the button
|
|
271
|
+
agent-browser fill @e3 "test@example.com" # Fill the textbox
|
|
272
|
+
agent-browser get text @e1 # Get heading text
|
|
273
|
+
agent-browser hover @e4 # Hover the link
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Why use refs?**
|
|
277
|
+
- **Deterministic**: Ref points to exact element from snapshot
|
|
278
|
+
- **Fast**: No DOM re-query needed
|
|
279
|
+
- **AI-friendly**: Snapshot + ref workflow is optimal for LLMs
|
|
280
|
+
|
|
281
|
+
### CSS Selectors
|
|
282
|
+
|
|
247
283
|
```bash
|
|
248
|
-
# CSS
|
|
249
284
|
agent-browser click "#id"
|
|
250
285
|
agent-browser click ".class"
|
|
251
286
|
agent-browser click "div > button"
|
|
287
|
+
```
|
|
252
288
|
|
|
253
|
-
|
|
254
|
-
agent-browser click "text=Submit"
|
|
289
|
+
### Text & XPath
|
|
255
290
|
|
|
256
|
-
|
|
291
|
+
```bash
|
|
292
|
+
agent-browser click "text=Submit"
|
|
257
293
|
agent-browser click "xpath=//button"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Semantic Locators
|
|
258
297
|
|
|
259
|
-
|
|
298
|
+
```bash
|
|
260
299
|
agent-browser find role button click --name "Submit"
|
|
261
300
|
agent-browser find label "Email" fill "test@test.com"
|
|
262
301
|
```
|
|
@@ -267,8 +306,26 @@ Use `--json` for machine-readable output:
|
|
|
267
306
|
|
|
268
307
|
```bash
|
|
269
308
|
agent-browser snapshot --json
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
# Returns: {"success":true,"data":{"snapshot":"...","refs":{"e1":{"role":"heading","name":"Title"},...}}}
|
|
310
|
+
|
|
311
|
+
agent-browser get text @e1 --json
|
|
312
|
+
agent-browser is visible @e2 --json
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Optimal AI Workflow
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# 1. Navigate and get snapshot
|
|
319
|
+
agent-browser open example.com
|
|
320
|
+
agent-browser snapshot --json # AI parses tree and refs
|
|
321
|
+
|
|
322
|
+
# 2. AI identifies target refs from snapshot
|
|
323
|
+
# 3. Execute actions using refs
|
|
324
|
+
agent-browser click @e2
|
|
325
|
+
agent-browser fill @e3 "input text"
|
|
326
|
+
|
|
327
|
+
# 4. Get new snapshot if page changed
|
|
328
|
+
agent-browser snapshot --json
|
|
272
329
|
```
|
|
273
330
|
|
|
274
331
|
## License
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Benchmark: agent-browser vs playwright-mcp
|
|
4
|
+
*
|
|
5
|
+
* Measures:
|
|
6
|
+
* - Speed: cold start, navigation, click, snapshot operations
|
|
7
|
+
* - Context usage: output/response size in bytes and estimated tokens
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn, execSync, ChildProcess } from 'child_process';
|
|
11
|
+
import * as readline from 'readline';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Configuration
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
const TEST_URL = 'https://example.com';
|
|
18
|
+
const ITERATIONS = 3;
|
|
19
|
+
|
|
20
|
+
interface BenchmarkResult {
|
|
21
|
+
operation: string;
|
|
22
|
+
tool: string;
|
|
23
|
+
timeMs: number;
|
|
24
|
+
outputBytes: number;
|
|
25
|
+
estimatedTokens: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const results: BenchmarkResult[] = [];
|
|
29
|
+
|
|
30
|
+
// Estimate tokens (~4 chars per token for English text)
|
|
31
|
+
function estimateTokens(text: string): number {
|
|
32
|
+
return Math.ceil(text.length / 4);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatBytes(bytes: number): string {
|
|
36
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
37
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
38
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Agent-Browser Benchmarks
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
async function runAgentBrowser(args: string[]): Promise<{ timeMs: number; output: string }> {
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const proc = spawn('node', ['./dist/index.js', '--session', 'benchmark', '--json', ...args], {
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
env: { ...process.env },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let output = '';
|
|
55
|
+
let stderr = '';
|
|
56
|
+
|
|
57
|
+
proc.stdout.on('data', (data) => { output += data.toString(); });
|
|
58
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
59
|
+
|
|
60
|
+
proc.on('close', (code) => {
|
|
61
|
+
const timeMs = performance.now() - start;
|
|
62
|
+
if (code !== 0 && !output.includes('"success"')) {
|
|
63
|
+
reject(new Error(`agent-browser failed: ${stderr || output}`));
|
|
64
|
+
} else {
|
|
65
|
+
resolve({ timeMs, output: output.trim() });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
proc.on('error', reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function benchmarkAgentBrowser(): Promise<void> {
|
|
74
|
+
console.log('\n📦 Benchmarking agent-browser...\n');
|
|
75
|
+
|
|
76
|
+
// Clean up any existing session
|
|
77
|
+
try {
|
|
78
|
+
await runAgentBrowser(['close']);
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore - session might not exist
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Wait a bit for cleanup
|
|
84
|
+
await new Promise(r => setTimeout(r, 500));
|
|
85
|
+
|
|
86
|
+
// Cold start (includes daemon startup + browser launch + navigation)
|
|
87
|
+
console.log(' ⏱️ Cold start (navigate)...');
|
|
88
|
+
const coldStart = await runAgentBrowser(['open', TEST_URL]);
|
|
89
|
+
results.push({
|
|
90
|
+
operation: 'cold_start_navigate',
|
|
91
|
+
tool: 'agent-browser',
|
|
92
|
+
timeMs: coldStart.timeMs,
|
|
93
|
+
outputBytes: coldStart.output.length,
|
|
94
|
+
estimatedTokens: estimateTokens(coldStart.output),
|
|
95
|
+
});
|
|
96
|
+
console.log(` ${coldStart.timeMs.toFixed(0)}ms, ${formatBytes(coldStart.output.length)}`);
|
|
97
|
+
|
|
98
|
+
// Warm operations
|
|
99
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
100
|
+
// Navigate (warm)
|
|
101
|
+
console.log(` ⏱️ Navigate (warm, iter ${i + 1})...`);
|
|
102
|
+
const nav = await runAgentBrowser(['open', TEST_URL]);
|
|
103
|
+
results.push({
|
|
104
|
+
operation: 'navigate_warm',
|
|
105
|
+
tool: 'agent-browser',
|
|
106
|
+
timeMs: nav.timeMs,
|
|
107
|
+
outputBytes: nav.output.length,
|
|
108
|
+
estimatedTokens: estimateTokens(nav.output),
|
|
109
|
+
});
|
|
110
|
+
console.log(` ${nav.timeMs.toFixed(0)}ms, ${formatBytes(nav.output.length)}`);
|
|
111
|
+
|
|
112
|
+
// Snapshot
|
|
113
|
+
console.log(` ⏱️ Snapshot (iter ${i + 1})...`);
|
|
114
|
+
const snapshot = await runAgentBrowser(['snapshot']);
|
|
115
|
+
results.push({
|
|
116
|
+
operation: 'snapshot',
|
|
117
|
+
tool: 'agent-browser',
|
|
118
|
+
timeMs: snapshot.timeMs,
|
|
119
|
+
outputBytes: snapshot.output.length,
|
|
120
|
+
estimatedTokens: estimateTokens(snapshot.output),
|
|
121
|
+
});
|
|
122
|
+
console.log(` ${snapshot.timeMs.toFixed(0)}ms, ${formatBytes(snapshot.output.length)}`);
|
|
123
|
+
|
|
124
|
+
// Get title
|
|
125
|
+
console.log(` ⏱️ Get title (iter ${i + 1})...`);
|
|
126
|
+
const title = await runAgentBrowser(['get', 'title']);
|
|
127
|
+
results.push({
|
|
128
|
+
operation: 'get_title',
|
|
129
|
+
tool: 'agent-browser',
|
|
130
|
+
timeMs: title.timeMs,
|
|
131
|
+
outputBytes: title.output.length,
|
|
132
|
+
estimatedTokens: estimateTokens(title.output),
|
|
133
|
+
});
|
|
134
|
+
console.log(` ${title.timeMs.toFixed(0)}ms, ${formatBytes(title.output.length)}`);
|
|
135
|
+
|
|
136
|
+
// Get URL
|
|
137
|
+
console.log(` ⏱️ Get URL (iter ${i + 1})...`);
|
|
138
|
+
const url = await runAgentBrowser(['get', 'url']);
|
|
139
|
+
results.push({
|
|
140
|
+
operation: 'get_url',
|
|
141
|
+
tool: 'agent-browser',
|
|
142
|
+
timeMs: url.timeMs,
|
|
143
|
+
outputBytes: url.output.length,
|
|
144
|
+
estimatedTokens: estimateTokens(url.output),
|
|
145
|
+
});
|
|
146
|
+
console.log(` ${url.timeMs.toFixed(0)}ms, ${formatBytes(url.output.length)}`);
|
|
147
|
+
|
|
148
|
+
// Click (on a link that exists on example.com)
|
|
149
|
+
console.log(` ⏱️ Click link (iter ${i + 1})...`);
|
|
150
|
+
const click = await runAgentBrowser(['click', 'a']);
|
|
151
|
+
results.push({
|
|
152
|
+
operation: 'click',
|
|
153
|
+
tool: 'agent-browser',
|
|
154
|
+
timeMs: click.timeMs,
|
|
155
|
+
outputBytes: click.output.length,
|
|
156
|
+
estimatedTokens: estimateTokens(click.output),
|
|
157
|
+
});
|
|
158
|
+
console.log(` ${click.timeMs.toFixed(0)}ms, ${formatBytes(click.output.length)}`);
|
|
159
|
+
|
|
160
|
+
// Navigate back for next iteration
|
|
161
|
+
await runAgentBrowser(['open', TEST_URL]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Screenshot
|
|
165
|
+
console.log(' ⏱️ Screenshot...');
|
|
166
|
+
const screenshot = await runAgentBrowser(['screenshot']);
|
|
167
|
+
results.push({
|
|
168
|
+
operation: 'screenshot',
|
|
169
|
+
tool: 'agent-browser',
|
|
170
|
+
timeMs: screenshot.timeMs,
|
|
171
|
+
outputBytes: screenshot.output.length,
|
|
172
|
+
estimatedTokens: estimateTokens(screenshot.output),
|
|
173
|
+
});
|
|
174
|
+
console.log(` ${screenshot.timeMs.toFixed(0)}ms, ${formatBytes(screenshot.output.length)}`);
|
|
175
|
+
|
|
176
|
+
// Close
|
|
177
|
+
console.log(' ⏱️ Close...');
|
|
178
|
+
const close = await runAgentBrowser(['close']);
|
|
179
|
+
results.push({
|
|
180
|
+
operation: 'close',
|
|
181
|
+
tool: 'agent-browser',
|
|
182
|
+
timeMs: close.timeMs,
|
|
183
|
+
outputBytes: close.output.length,
|
|
184
|
+
estimatedTokens: estimateTokens(close.output),
|
|
185
|
+
});
|
|
186
|
+
console.log(` ${close.timeMs.toFixed(0)}ms`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Playwright-MCP Benchmarks
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
class MCPClient {
|
|
194
|
+
private proc: ChildProcess;
|
|
195
|
+
private rl: readline.Interface;
|
|
196
|
+
private responseBuffer: Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }> = new Map();
|
|
197
|
+
private requestId = 0;
|
|
198
|
+
private ready = false;
|
|
199
|
+
|
|
200
|
+
constructor() {
|
|
201
|
+
this.proc = spawn('node', ['./opensrc/repos/github.com/microsoft/playwright-mcp/cli.js', '--headless'], {
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.rl = readline.createInterface({ input: this.proc.stdout! });
|
|
207
|
+
|
|
208
|
+
this.rl.on('line', (line) => {
|
|
209
|
+
try {
|
|
210
|
+
const msg = JSON.parse(line);
|
|
211
|
+
if (msg.id !== undefined && this.responseBuffer.has(msg.id)) {
|
|
212
|
+
const handler = this.responseBuffer.get(msg.id)!;
|
|
213
|
+
this.responseBuffer.delete(msg.id);
|
|
214
|
+
handler.resolve(msg);
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Non-JSON output, ignore
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.proc.stderr?.on('data', (data) => {
|
|
222
|
+
// Debug output, ignore in benchmarks
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async initialize(): Promise<{ timeMs: number; output: string }> {
|
|
227
|
+
const start = performance.now();
|
|
228
|
+
|
|
229
|
+
// Send initialize request
|
|
230
|
+
const initResult = await this.sendRequest('initialize', {
|
|
231
|
+
protocolVersion: '2024-11-05',
|
|
232
|
+
capabilities: {},
|
|
233
|
+
clientInfo: { name: 'benchmark', version: '1.0.0' },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Send initialized notification
|
|
237
|
+
this.sendNotification('notifications/initialized', {});
|
|
238
|
+
|
|
239
|
+
const timeMs = performance.now() - start;
|
|
240
|
+
const output = JSON.stringify(initResult);
|
|
241
|
+
|
|
242
|
+
this.ready = true;
|
|
243
|
+
return { timeMs, output };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async callTool(name: string, args: Record<string, unknown>): Promise<{ timeMs: number; output: string }> {
|
|
247
|
+
const start = performance.now();
|
|
248
|
+
const result = await this.sendRequest('tools/call', { name, arguments: args });
|
|
249
|
+
const timeMs = performance.now() - start;
|
|
250
|
+
const output = JSON.stringify(result);
|
|
251
|
+
return { timeMs, output };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private sendRequest(method: string, params: Record<string, unknown>): Promise<any> {
|
|
255
|
+
const id = ++this.requestId;
|
|
256
|
+
const request = { jsonrpc: '2.0', id, method, params };
|
|
257
|
+
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
this.responseBuffer.set(id, { resolve, reject });
|
|
260
|
+
this.proc.stdin!.write(JSON.stringify(request) + '\n');
|
|
261
|
+
|
|
262
|
+
// Timeout after 30s
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
if (this.responseBuffer.has(id)) {
|
|
265
|
+
this.responseBuffer.delete(id);
|
|
266
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
267
|
+
}
|
|
268
|
+
}, 30000);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private sendNotification(method: string, params: Record<string, unknown>): void {
|
|
273
|
+
const notification = { jsonrpc: '2.0', method, params };
|
|
274
|
+
this.proc.stdin!.write(JSON.stringify(notification) + '\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async close(): Promise<void> {
|
|
278
|
+
this.proc.kill();
|
|
279
|
+
this.rl.close();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function benchmarkPlaywrightMCP(): Promise<void> {
|
|
284
|
+
console.log('\n📦 Benchmarking playwright-mcp...\n');
|
|
285
|
+
|
|
286
|
+
let client: MCPClient | null = null;
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
// Cold start (includes server startup + initialization)
|
|
290
|
+
console.log(' ⏱️ Cold start (initialize + navigate)...');
|
|
291
|
+
const coldStartBegin = performance.now();
|
|
292
|
+
|
|
293
|
+
client = new MCPClient();
|
|
294
|
+
const init = await client.initialize();
|
|
295
|
+
|
|
296
|
+
// Navigate
|
|
297
|
+
const nav = await client.callTool('browser_navigate', { url: TEST_URL });
|
|
298
|
+
|
|
299
|
+
const coldStartTime = performance.now() - coldStartBegin;
|
|
300
|
+
const coldStartOutput = init.output + nav.output;
|
|
301
|
+
|
|
302
|
+
results.push({
|
|
303
|
+
operation: 'cold_start_navigate',
|
|
304
|
+
tool: 'playwright-mcp',
|
|
305
|
+
timeMs: coldStartTime,
|
|
306
|
+
outputBytes: coldStartOutput.length,
|
|
307
|
+
estimatedTokens: estimateTokens(coldStartOutput),
|
|
308
|
+
});
|
|
309
|
+
console.log(` ${coldStartTime.toFixed(0)}ms, ${formatBytes(coldStartOutput.length)}`);
|
|
310
|
+
|
|
311
|
+
// Warm operations
|
|
312
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
313
|
+
// Navigate (warm)
|
|
314
|
+
console.log(` ⏱️ Navigate (warm, iter ${i + 1})...`);
|
|
315
|
+
const navWarm = await client.callTool('browser_navigate', { url: TEST_URL });
|
|
316
|
+
results.push({
|
|
317
|
+
operation: 'navigate_warm',
|
|
318
|
+
tool: 'playwright-mcp',
|
|
319
|
+
timeMs: navWarm.timeMs,
|
|
320
|
+
outputBytes: navWarm.output.length,
|
|
321
|
+
estimatedTokens: estimateTokens(navWarm.output),
|
|
322
|
+
});
|
|
323
|
+
console.log(` ${navWarm.timeMs.toFixed(0)}ms, ${formatBytes(navWarm.output.length)}`);
|
|
324
|
+
|
|
325
|
+
// Snapshot
|
|
326
|
+
console.log(` ⏱️ Snapshot (iter ${i + 1})...`);
|
|
327
|
+
const snapshot = await client.callTool('browser_snapshot', {});
|
|
328
|
+
results.push({
|
|
329
|
+
operation: 'snapshot',
|
|
330
|
+
tool: 'playwright-mcp',
|
|
331
|
+
timeMs: snapshot.timeMs,
|
|
332
|
+
outputBytes: snapshot.output.length,
|
|
333
|
+
estimatedTokens: estimateTokens(snapshot.output),
|
|
334
|
+
});
|
|
335
|
+
console.log(` ${snapshot.timeMs.toFixed(0)}ms, ${formatBytes(snapshot.output.length)}`);
|
|
336
|
+
|
|
337
|
+
// Note: playwright-mcp doesn't have separate get_title/get_url tools
|
|
338
|
+
// Title and URL are included in snapshot, so we'll skip those
|
|
339
|
+
|
|
340
|
+
// Click
|
|
341
|
+
console.log(` ⏱️ Click link (iter ${i + 1})...`);
|
|
342
|
+
// playwright-mcp uses ref from snapshot - we'll use a generic approach
|
|
343
|
+
const click = await client.callTool('browser_click', {
|
|
344
|
+
element: 'More information link',
|
|
345
|
+
ref: 'a' // This might not work exactly the same way
|
|
346
|
+
});
|
|
347
|
+
results.push({
|
|
348
|
+
operation: 'click',
|
|
349
|
+
tool: 'playwright-mcp',
|
|
350
|
+
timeMs: click.timeMs,
|
|
351
|
+
outputBytes: click.output.length,
|
|
352
|
+
estimatedTokens: estimateTokens(click.output),
|
|
353
|
+
});
|
|
354
|
+
console.log(` ${click.timeMs.toFixed(0)}ms, ${formatBytes(click.output.length)}`);
|
|
355
|
+
|
|
356
|
+
// Navigate back
|
|
357
|
+
await client.callTool('browser_navigate', { url: TEST_URL });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Screenshot
|
|
361
|
+
console.log(' ⏱️ Screenshot...');
|
|
362
|
+
const screenshot = await client.callTool('browser_take_screenshot', {});
|
|
363
|
+
results.push({
|
|
364
|
+
operation: 'screenshot',
|
|
365
|
+
tool: 'playwright-mcp',
|
|
366
|
+
timeMs: screenshot.timeMs,
|
|
367
|
+
outputBytes: screenshot.output.length,
|
|
368
|
+
estimatedTokens: estimateTokens(screenshot.output),
|
|
369
|
+
});
|
|
370
|
+
console.log(` ${screenshot.timeMs.toFixed(0)}ms, ${formatBytes(screenshot.output.length)}`);
|
|
371
|
+
|
|
372
|
+
// Close
|
|
373
|
+
console.log(' ⏱️ Close...');
|
|
374
|
+
const closeStart = performance.now();
|
|
375
|
+
const closeResult = await client.callTool('browser_close', {});
|
|
376
|
+
results.push({
|
|
377
|
+
operation: 'close',
|
|
378
|
+
tool: 'playwright-mcp',
|
|
379
|
+
timeMs: closeResult.timeMs,
|
|
380
|
+
outputBytes: closeResult.output.length,
|
|
381
|
+
estimatedTokens: estimateTokens(closeResult.output),
|
|
382
|
+
});
|
|
383
|
+
console.log(` ${closeResult.timeMs.toFixed(0)}ms`);
|
|
384
|
+
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error(' ❌ playwright-mcp benchmark failed:', error);
|
|
387
|
+
} finally {
|
|
388
|
+
if (client) {
|
|
389
|
+
await client.close();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// Results Summary
|
|
396
|
+
// ============================================================================
|
|
397
|
+
|
|
398
|
+
function printResults(): void {
|
|
399
|
+
console.log('\n' + '='.repeat(80));
|
|
400
|
+
console.log('📊 BENCHMARK RESULTS');
|
|
401
|
+
console.log('='.repeat(80));
|
|
402
|
+
|
|
403
|
+
// Group by operation
|
|
404
|
+
const operations = [...new Set(results.map(r => r.operation))];
|
|
405
|
+
|
|
406
|
+
console.log('\n📈 Speed Comparison (average across iterations):\n');
|
|
407
|
+
console.log('| Operation | agent-browser | playwright-mcp | Difference |');
|
|
408
|
+
console.log('|---------------------|---------------|----------------|------------|');
|
|
409
|
+
|
|
410
|
+
for (const op of operations) {
|
|
411
|
+
const agentResults = results.filter(r => r.operation === op && r.tool === 'agent-browser');
|
|
412
|
+
const mcpResults = results.filter(r => r.operation === op && r.tool === 'playwright-mcp');
|
|
413
|
+
|
|
414
|
+
const agentAvg = agentResults.length > 0
|
|
415
|
+
? agentResults.reduce((sum, r) => sum + r.timeMs, 0) / agentResults.length
|
|
416
|
+
: null;
|
|
417
|
+
const mcpAvg = mcpResults.length > 0
|
|
418
|
+
? mcpResults.reduce((sum, r) => sum + r.timeMs, 0) / mcpResults.length
|
|
419
|
+
: null;
|
|
420
|
+
|
|
421
|
+
const agentStr = agentAvg !== null ? `${agentAvg.toFixed(0)}ms`.padEnd(13) : 'N/A'.padEnd(13);
|
|
422
|
+
const mcpStr = mcpAvg !== null ? `${mcpAvg.toFixed(0)}ms`.padEnd(14) : 'N/A'.padEnd(14);
|
|
423
|
+
|
|
424
|
+
let diff = '';
|
|
425
|
+
if (agentAvg !== null && mcpAvg !== null) {
|
|
426
|
+
const ratio = agentAvg / mcpAvg;
|
|
427
|
+
if (ratio < 1) {
|
|
428
|
+
diff = `${((1 - ratio) * 100).toFixed(0)}% faster`;
|
|
429
|
+
} else if (ratio > 1) {
|
|
430
|
+
diff = `${((ratio - 1) * 100).toFixed(0)}% slower`;
|
|
431
|
+
} else {
|
|
432
|
+
diff = 'same';
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log(`| ${op.padEnd(19)} | ${agentStr} | ${mcpStr} | ${diff.padEnd(10)} |`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log('\n📦 Context Usage (output size for AI consumption):\n');
|
|
440
|
+
console.log('| Operation | agent-browser | playwright-mcp |');
|
|
441
|
+
console.log('|---------------------|--------------------|--------------------|');
|
|
442
|
+
|
|
443
|
+
for (const op of operations) {
|
|
444
|
+
const agentResults = results.filter(r => r.operation === op && r.tool === 'agent-browser');
|
|
445
|
+
const mcpResults = results.filter(r => r.operation === op && r.tool === 'playwright-mcp');
|
|
446
|
+
|
|
447
|
+
const agentAvgBytes = agentResults.length > 0
|
|
448
|
+
? agentResults.reduce((sum, r) => sum + r.outputBytes, 0) / agentResults.length
|
|
449
|
+
: null;
|
|
450
|
+
const agentAvgTokens = agentResults.length > 0
|
|
451
|
+
? agentResults.reduce((sum, r) => sum + r.estimatedTokens, 0) / agentResults.length
|
|
452
|
+
: null;
|
|
453
|
+
|
|
454
|
+
const mcpAvgBytes = mcpResults.length > 0
|
|
455
|
+
? mcpResults.reduce((sum, r) => sum + r.outputBytes, 0) / mcpResults.length
|
|
456
|
+
: null;
|
|
457
|
+
const mcpAvgTokens = mcpResults.length > 0
|
|
458
|
+
? mcpResults.reduce((sum, r) => sum + r.estimatedTokens, 0) / mcpResults.length
|
|
459
|
+
: null;
|
|
460
|
+
|
|
461
|
+
const agentStr = agentAvgBytes !== null
|
|
462
|
+
? `${formatBytes(agentAvgBytes)} (~${Math.round(agentAvgTokens!)} tok)`.padEnd(18)
|
|
463
|
+
: 'N/A'.padEnd(18);
|
|
464
|
+
const mcpStr = mcpAvgBytes !== null
|
|
465
|
+
? `${formatBytes(mcpAvgBytes)} (~${Math.round(mcpAvgTokens!)} tok)`.padEnd(18)
|
|
466
|
+
: 'N/A'.padEnd(18);
|
|
467
|
+
|
|
468
|
+
console.log(`| ${op.padEnd(19)} | ${agentStr} | ${mcpStr} |`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Total context usage
|
|
472
|
+
const agentTotal = results.filter(r => r.tool === 'agent-browser');
|
|
473
|
+
const mcpTotal = results.filter(r => r.tool === 'playwright-mcp');
|
|
474
|
+
|
|
475
|
+
const agentTotalBytes = agentTotal.reduce((sum, r) => sum + r.outputBytes, 0);
|
|
476
|
+
const agentTotalTokens = agentTotal.reduce((sum, r) => sum + r.estimatedTokens, 0);
|
|
477
|
+
const mcpTotalBytes = mcpTotal.reduce((sum, r) => sum + r.outputBytes, 0);
|
|
478
|
+
const mcpTotalTokens = mcpTotal.reduce((sum, r) => sum + r.estimatedTokens, 0);
|
|
479
|
+
|
|
480
|
+
console.log('\n📊 Total Context Usage (all operations combined):');
|
|
481
|
+
console.log(` agent-browser: ${formatBytes(agentTotalBytes)} (~${agentTotalTokens} tokens)`);
|
|
482
|
+
console.log(` playwright-mcp: ${formatBytes(mcpTotalBytes)} (~${mcpTotalTokens} tokens)`);
|
|
483
|
+
|
|
484
|
+
if (agentTotalBytes > 0 && mcpTotalBytes > 0) {
|
|
485
|
+
const ratio = agentTotalBytes / mcpTotalBytes;
|
|
486
|
+
if (ratio < 1) {
|
|
487
|
+
console.log(` → agent-browser uses ${((1 - ratio) * 100).toFixed(0)}% less context`);
|
|
488
|
+
} else {
|
|
489
|
+
console.log(` → playwright-mcp uses ${((1 - 1/ratio) * 100).toFixed(0)}% less context`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
console.log('\n' + '='.repeat(80));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ============================================================================
|
|
497
|
+
// Main
|
|
498
|
+
// ============================================================================
|
|
499
|
+
|
|
500
|
+
async function main(): Promise<void> {
|
|
501
|
+
console.log('🚀 Browser Automation Benchmark');
|
|
502
|
+
console.log(` Testing against: ${TEST_URL}`);
|
|
503
|
+
console.log(` Iterations: ${ITERATIONS}`);
|
|
504
|
+
console.log('='.repeat(80));
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
// Build first
|
|
508
|
+
console.log('\n🔨 Building agent-browser...');
|
|
509
|
+
execSync('pnpm build', { cwd: process.cwd(), stdio: 'inherit' });
|
|
510
|
+
|
|
511
|
+
await benchmarkAgentBrowser();
|
|
512
|
+
await benchmarkPlaywrightMCP();
|
|
513
|
+
|
|
514
|
+
printResults();
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.error('\n❌ Benchmark failed:', error);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
main();
|