browser-use 0.4.0 → 0.6.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.
Files changed (58) hide show
  1. package/dist/agent/service.js +2 -0
  2. package/dist/agent/system_prompt.md +269 -0
  3. package/dist/agent/system_prompt_anthropic_flash.md +240 -0
  4. package/dist/agent/system_prompt_browser_use.md +18 -0
  5. package/dist/agent/system_prompt_browser_use_flash.md +15 -0
  6. package/dist/agent/system_prompt_browser_use_no_thinking.md +17 -0
  7. package/dist/agent/system_prompt_flash.md +16 -0
  8. package/dist/agent/system_prompt_flash_anthropic.md +30 -0
  9. package/dist/agent/system_prompt_no_thinking.md +245 -0
  10. package/dist/browser/cloud/index.d.ts +1 -0
  11. package/dist/browser/cloud/index.js +1 -0
  12. package/dist/browser/cloud/management.d.ts +130 -0
  13. package/dist/browser/cloud/management.js +140 -0
  14. package/dist/browser/events.d.ts +61 -3
  15. package/dist/browser/events.js +66 -0
  16. package/dist/browser/profile.d.ts +1 -0
  17. package/dist/browser/profile.js +25 -8
  18. package/dist/browser/session.d.ts +59 -2
  19. package/dist/browser/session.js +943 -131
  20. package/dist/browser/watchdogs/base.js +34 -1
  21. package/dist/browser/watchdogs/captcha-watchdog.d.ts +26 -0
  22. package/dist/browser/watchdogs/captcha-watchdog.js +151 -0
  23. package/dist/browser/watchdogs/index.d.ts +1 -0
  24. package/dist/browser/watchdogs/index.js +1 -0
  25. package/dist/browser/watchdogs/screenshot-watchdog.js +4 -3
  26. package/dist/cli.d.ts +120 -0
  27. package/dist/cli.js +1816 -4
  28. package/dist/controller/service.js +106 -362
  29. package/dist/controller/views.d.ts +9 -6
  30. package/dist/controller/views.js +8 -5
  31. package/dist/dom/dom_tree/index.js +24 -11
  32. package/dist/filesystem/file-system.js +1 -1
  33. package/dist/llm/litellm/chat.d.ts +11 -0
  34. package/dist/llm/litellm/chat.js +16 -0
  35. package/dist/llm/litellm/index.d.ts +1 -0
  36. package/dist/llm/litellm/index.js +1 -0
  37. package/dist/llm/models.js +29 -3
  38. package/dist/llm/oci-raw/chat.d.ts +64 -0
  39. package/dist/llm/oci-raw/chat.js +350 -0
  40. package/dist/llm/oci-raw/index.d.ts +2 -0
  41. package/dist/llm/oci-raw/index.js +2 -0
  42. package/dist/llm/oci-raw/serializer.d.ts +12 -0
  43. package/dist/llm/oci-raw/serializer.js +128 -0
  44. package/dist/mcp/server.d.ts +1 -0
  45. package/dist/mcp/server.js +62 -13
  46. package/dist/skill-cli/direct.d.ts +100 -0
  47. package/dist/skill-cli/direct.js +984 -0
  48. package/dist/skill-cli/index.d.ts +2 -0
  49. package/dist/skill-cli/index.js +2 -0
  50. package/dist/skill-cli/server.d.ts +2 -0
  51. package/dist/skill-cli/server.js +472 -11
  52. package/dist/skill-cli/tunnel.d.ts +61 -0
  53. package/dist/skill-cli/tunnel.js +257 -0
  54. package/dist/sync/auth.d.ts +8 -0
  55. package/dist/sync/auth.js +12 -0
  56. package/dist/utils.d.ts +1 -1
  57. package/dist/utils.js +2 -1
  58. package/package.json +22 -4
@@ -0,0 +1,984 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import net from 'node:net';
6
+ import { spawn } from 'node:child_process';
7
+ import { BrowserSession, systemChrome } from '../browser/session.js';
8
+ import { CloudBrowserClient } from '../browser/cloud/cloud.js';
9
+ export const DIRECT_STATE_FILE = path.join(os.tmpdir(), 'browser-use-direct.json');
10
+ const normalizeCookieDomain = (value) => String(value ?? '')
11
+ .trim()
12
+ .replace(/^\./, '')
13
+ .toLowerCase();
14
+ const parseCookieHostname = (url) => {
15
+ const value = String(url ?? '').trim();
16
+ if (!value) {
17
+ return '';
18
+ }
19
+ try {
20
+ return new URL(value).hostname.toLowerCase();
21
+ }
22
+ catch {
23
+ return '';
24
+ }
25
+ };
26
+ const parseCookieUrl = (url) => {
27
+ const value = String(url ?? '').trim();
28
+ if (!value) {
29
+ return null;
30
+ }
31
+ try {
32
+ return new URL(value);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ };
38
+ const cookiePathMatches = (cookiePath, urlPath) => {
39
+ const normalizedCookiePath = typeof cookiePath === 'string' && cookiePath.length > 0 ? cookiePath : '/';
40
+ if (normalizedCookiePath === '/') {
41
+ return true;
42
+ }
43
+ if (urlPath === normalizedCookiePath) {
44
+ return true;
45
+ }
46
+ return urlPath.startsWith(normalizedCookiePath.endsWith('/')
47
+ ? normalizedCookiePath
48
+ : `${normalizedCookiePath}/`);
49
+ };
50
+ const cookieMatchesUrl = (cookie, url) => {
51
+ const parsedUrl = parseCookieUrl(url);
52
+ const hostname = parsedUrl?.hostname.toLowerCase() ?? '';
53
+ const domain = normalizeCookieDomain(cookie.domain);
54
+ if (!hostname || !domain) {
55
+ return false;
56
+ }
57
+ if (!(hostname === domain ||
58
+ hostname.endsWith(`.${domain}`) ||
59
+ domain.endsWith(`.${hostname}`))) {
60
+ return false;
61
+ }
62
+ if (!cookiePathMatches(cookie.path, parsedUrl?.pathname || '/')) {
63
+ return false;
64
+ }
65
+ if (cookie.secure && parsedUrl?.protocol !== 'https:') {
66
+ return false;
67
+ }
68
+ return true;
69
+ };
70
+ const normalizeSameSite = (value) => {
71
+ const normalized = String(value ?? '')
72
+ .trim()
73
+ .toLowerCase();
74
+ if (normalized === 'strict') {
75
+ return 'Strict';
76
+ }
77
+ if (normalized === 'lax') {
78
+ return 'Lax';
79
+ }
80
+ if (normalized === 'none') {
81
+ return 'None';
82
+ }
83
+ return undefined;
84
+ };
85
+ const DEFAULT_STDOUT = process.stdout;
86
+ const DEFAULT_STDERR = process.stderr;
87
+ const writeLine = (stream, message) => {
88
+ stream.write(`${message}\n`);
89
+ };
90
+ export const load_direct_state = (state_file = DIRECT_STATE_FILE) => {
91
+ if (!fs.existsSync(state_file)) {
92
+ return {};
93
+ }
94
+ try {
95
+ return JSON.parse(fs.readFileSync(state_file, 'utf8'));
96
+ }
97
+ catch {
98
+ return {};
99
+ }
100
+ };
101
+ export const save_direct_state = (state, state_file = DIRECT_STATE_FILE) => {
102
+ fs.writeFileSync(state_file, JSON.stringify(state, null, 2));
103
+ };
104
+ export const clear_direct_state = (state_file = DIRECT_STATE_FILE) => {
105
+ fs.rmSync(state_file, { force: true });
106
+ };
107
+ const cleanupOwnedDirectUserDataDir = (state) => {
108
+ if (!state.owns_user_data_dir || !state.user_data_dir) {
109
+ return;
110
+ }
111
+ try {
112
+ fs.rmSync(state.user_data_dir, { recursive: true, force: true });
113
+ }
114
+ catch {
115
+ // Ignore cleanup failures for ephemeral direct-mode profiles.
116
+ }
117
+ };
118
+ const normalizeDirectUrl = (input) => {
119
+ const trimmed = input.trim();
120
+ if (!trimmed) {
121
+ return trimmed;
122
+ }
123
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
124
+ return trimmed;
125
+ }
126
+ return `https://${trimmed}`;
127
+ };
128
+ const formatDirectUsage = () => `Usage: browser-use-direct <command> [args]
129
+
130
+ Commands:
131
+ open <url> Navigate to URL
132
+ state Get current browser state
133
+ click <index> Click element by DOM index
134
+ click <x> <y> Click viewport coordinates
135
+ type <text> Type into focused element
136
+ input <index> <text> Click element and type text
137
+ screenshot [path] Take screenshot
138
+ scroll [up|down] Scroll page
139
+ back Go back in history
140
+ forward Go forward in history
141
+ switch <tab> Switch to tab index or target id
142
+ close-tab [tab] Close a tab
143
+ keys <keys> Send keyboard keys
144
+ select <index> <value> Select dropdown option
145
+ wait selector <css> Wait for a selector
146
+ wait text <text> Wait for text
147
+ hover <index> Hover element by DOM index
148
+ dblclick <index> Double-click element by DOM index
149
+ rightclick <index> Right-click element by DOM index
150
+ cookies <subcommand> Manage cookies (get/set/clear/export/import)
151
+ get title Get page title
152
+ get html [selector] Get page HTML or a CSS selector
153
+ get text <index> Get element text
154
+ get value <index> Get element value
155
+ get attributes <index> Get element attributes
156
+ get bbox <index> Get element bounding box
157
+ extract <query> Explain that extraction requires agent mode
158
+ html [selector] Get page HTML or a CSS selector
159
+ eval <js> Execute JavaScript
160
+ close Close the active direct-mode browser
161
+
162
+ Flags:
163
+ --remote Launch browser-use cloud browser`;
164
+ const extractDirectModeArgs = (argv) => {
165
+ let useRemote = false;
166
+ let index = 0;
167
+ while (index < argv.length) {
168
+ const arg = argv[index] ?? '';
169
+ if (arg === '--remote') {
170
+ useRemote = true;
171
+ index += 1;
172
+ continue;
173
+ }
174
+ break;
175
+ }
176
+ return {
177
+ useRemote,
178
+ args: argv.slice(index),
179
+ };
180
+ };
181
+ const getFreePort = async () => await new Promise((resolve, reject) => {
182
+ const server = net.createServer();
183
+ server.once('error', reject);
184
+ server.listen(0, '127.0.0.1', () => {
185
+ const address = server.address();
186
+ if (!address || typeof address === 'string') {
187
+ server.close(() => reject(new Error('Failed to allocate local port')));
188
+ return;
189
+ }
190
+ const port = address.port;
191
+ server.close((error) => {
192
+ if (error) {
193
+ reject(error);
194
+ return;
195
+ }
196
+ resolve(port);
197
+ });
198
+ });
199
+ });
200
+ const waitForLocalCdpEndpoint = async (port, timeoutMs = 15000) => {
201
+ const deadline = Date.now() + timeoutMs;
202
+ const endpoint = `http://127.0.0.1:${port}`;
203
+ while (Date.now() < deadline) {
204
+ try {
205
+ const response = await fetch(`${endpoint}/json/version`);
206
+ if (response.ok) {
207
+ const payload = (await response.json());
208
+ if (typeof payload.webSocketDebuggerUrl === 'string') {
209
+ return endpoint;
210
+ }
211
+ }
212
+ }
213
+ catch {
214
+ // Keep polling until timeout.
215
+ }
216
+ await new Promise((resolve) => setTimeout(resolve, 100));
217
+ }
218
+ throw new Error(`Timed out waiting for local Chrome debugging endpoint on port ${port}`);
219
+ };
220
+ export const defaultLocalLauncher = async (options) => {
221
+ const executablePath = systemChrome.findExecutable();
222
+ if (!executablePath) {
223
+ throw new Error('Chrome not found. Install Chrome or provide an already-running browser via cdp_url.');
224
+ }
225
+ const port = await getFreePort();
226
+ const reusingUserDataDir = options.state.user_data_dir &&
227
+ options.state.user_data_dir.trim().length > 0;
228
+ const userDataDir = reusingUserDataDir
229
+ ? options.state.user_data_dir
230
+ : fs.mkdtempSync(path.join(os.tmpdir(), 'browser-use-direct-'));
231
+ const child = spawn(executablePath, [
232
+ `--remote-debugging-port=${port}`,
233
+ `--user-data-dir=${userDataDir}`,
234
+ '--no-first-run',
235
+ '--no-default-browser-check',
236
+ 'about:blank',
237
+ ], {
238
+ detached: true,
239
+ stdio: 'ignore',
240
+ });
241
+ child.unref();
242
+ try {
243
+ const cdp_url = await waitForLocalCdpEndpoint(port, options.timeout_ms);
244
+ return {
245
+ cdp_url,
246
+ browser_pid: child.pid ?? null,
247
+ user_data_dir: userDataDir,
248
+ owns_user_data_dir: !reusingUserDataDir,
249
+ };
250
+ }
251
+ catch (error) {
252
+ if (typeof child.pid === 'number' && child.pid > 0) {
253
+ try {
254
+ process.kill(child.pid, 'SIGTERM');
255
+ }
256
+ catch {
257
+ // Ignore cleanup failures for a process that may not have started.
258
+ }
259
+ }
260
+ if (!reusingUserDataDir && typeof userDataDir === 'string') {
261
+ try {
262
+ fs.rmSync(userDataDir, { recursive: true, force: true });
263
+ }
264
+ catch {
265
+ // Ignore cleanup failures for ephemeral launch profiles.
266
+ }
267
+ }
268
+ throw error;
269
+ }
270
+ };
271
+ const cleanupDirectSession = async (session) => {
272
+ try {
273
+ session.detach_all_watchdogs?.();
274
+ }
275
+ catch {
276
+ // Ignore cleanup failures.
277
+ }
278
+ try {
279
+ await session.event_bus?.stop?.();
280
+ }
281
+ catch {
282
+ // Ignore event bus cleanup failures.
283
+ }
284
+ };
285
+ const requireDirectNodeByIndex = async (session, indexValue) => {
286
+ const index = Number(indexValue ?? Number.NaN);
287
+ if (!Number.isFinite(index)) {
288
+ throw new Error('Missing index');
289
+ }
290
+ const node = await session.get_dom_element_by_index?.(index);
291
+ if (!node) {
292
+ throw new Error(`Element index ${index} not found - run "state" first`);
293
+ }
294
+ return { index, node };
295
+ };
296
+ const readDirectNodeData = async (session, node, kind) => {
297
+ if (!node?.xpath) {
298
+ throw new Error('DOM element does not include an XPath selector');
299
+ }
300
+ const page = await session.get_current_page?.();
301
+ if (!page?.evaluate) {
302
+ throw new Error('No active page available');
303
+ }
304
+ return await page.evaluate(({ xpath, dataKind }) => {
305
+ const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
306
+ if (!element) {
307
+ return null;
308
+ }
309
+ if (dataKind === 'text') {
310
+ return element.textContent?.trim() ?? '';
311
+ }
312
+ if (dataKind === 'value') {
313
+ return 'value' in element
314
+ ? String(element.value ?? '')
315
+ : null;
316
+ }
317
+ if (dataKind === 'attributes') {
318
+ return Object.fromEntries(Array.from(element.attributes).map((attribute) => [
319
+ attribute.name,
320
+ attribute.value,
321
+ ]));
322
+ }
323
+ if (dataKind === 'bbox') {
324
+ const rect = element.getBoundingClientRect();
325
+ return {
326
+ x: rect.x,
327
+ y: rect.y,
328
+ width: rect.width,
329
+ height: rect.height,
330
+ top: rect.top,
331
+ right: rect.right,
332
+ bottom: rect.bottom,
333
+ left: rect.left,
334
+ };
335
+ }
336
+ return null;
337
+ }, { xpath: node.xpath, dataKind: kind });
338
+ };
339
+ const takeDirectOptionValue = (args, index, option) => {
340
+ const next = args[index + 1]?.trim();
341
+ if (!next || next === '--' || next.startsWith('-')) {
342
+ throw new Error(`Missing value for ${option}`);
343
+ }
344
+ return next;
345
+ };
346
+ const parseDirectCookieOptions = (args) => {
347
+ const positional = [];
348
+ let url = null;
349
+ let domain = null;
350
+ let cookiePath = '/';
351
+ let secure = false;
352
+ let httpOnly = false;
353
+ let sameSite;
354
+ let expires;
355
+ for (let index = 0; index < args.length; index += 1) {
356
+ const arg = args[index] ?? '';
357
+ if (arg === '--') {
358
+ positional.push(...args.slice(index + 1));
359
+ break;
360
+ }
361
+ if (arg === '--url' ||
362
+ arg === '--domain' ||
363
+ arg === '--path' ||
364
+ arg === '--same-site' ||
365
+ arg === '--expires') {
366
+ const next = takeDirectOptionValue(args, index, arg);
367
+ if (arg === '--url') {
368
+ url = next;
369
+ }
370
+ else if (arg === '--domain') {
371
+ domain = next;
372
+ }
373
+ else if (arg === '--path') {
374
+ cookiePath = next;
375
+ }
376
+ else if (arg === '--same-site') {
377
+ sameSite = normalizeSameSite(next);
378
+ if (!sameSite) {
379
+ throw new Error('Invalid --same-site value. Expected Strict, Lax, or None');
380
+ }
381
+ }
382
+ else {
383
+ const parsed = Number(next);
384
+ if (!Number.isFinite(parsed)) {
385
+ throw new Error(`Invalid --expires value: ${next}`);
386
+ }
387
+ expires = parsed;
388
+ }
389
+ index += 1;
390
+ continue;
391
+ }
392
+ if (arg === '--secure') {
393
+ secure = true;
394
+ continue;
395
+ }
396
+ if (arg === '--http-only') {
397
+ httpOnly = true;
398
+ continue;
399
+ }
400
+ if (arg.startsWith('-')) {
401
+ throw new Error(`Unknown option: ${arg}`);
402
+ }
403
+ positional.push(arg);
404
+ }
405
+ return {
406
+ positional,
407
+ url,
408
+ domain,
409
+ path: cookiePath,
410
+ secure,
411
+ httpOnly,
412
+ sameSite,
413
+ expires,
414
+ };
415
+ };
416
+ const restoreActiveTab = async (session, state) => {
417
+ if (typeof state.active_url !== 'string' ||
418
+ !state.active_url ||
419
+ !Array.isArray(session.tabs) ||
420
+ typeof session.switch_to_tab !== 'function') {
421
+ return;
422
+ }
423
+ const matchingTab = session.tabs.find((tab) => tab?.url === state.active_url);
424
+ if (!matchingTab?.target_id) {
425
+ return;
426
+ }
427
+ try {
428
+ await session.switch_to_tab(matchingTab.target_id);
429
+ }
430
+ catch {
431
+ // Fall back to the default page if the tab cannot be restored.
432
+ }
433
+ };
434
+ const createDefaultSessionFactory = () => (init) => new BrowserSession({
435
+ cdp_url: init.cdp_url ?? null,
436
+ profile: {
437
+ keep_alive: true,
438
+ },
439
+ });
440
+ const connectDirectSession = async (useRemote, environment) => {
441
+ let state = load_direct_state(environment.state_file);
442
+ const session_factory = environment.session_factory ?? createDefaultSessionFactory();
443
+ const connectWithState = async (currentState) => {
444
+ const session = session_factory({ cdp_url: currentState.cdp_url ?? null });
445
+ await session.start();
446
+ await restoreActiveTab(session, currentState);
447
+ return session;
448
+ };
449
+ const cleanupDisconnectedState = async (currentState) => {
450
+ if (currentState.mode === 'remote' && currentState.session_id) {
451
+ try {
452
+ await environment
453
+ .cloud_client_factory()
454
+ .stop_browser(currentState.session_id);
455
+ }
456
+ catch {
457
+ // Best-effort cleanup for stale remote sessions.
458
+ }
459
+ return;
460
+ }
461
+ if (currentState.mode === 'local' &&
462
+ typeof currentState.browser_pid === 'number' &&
463
+ currentState.browser_pid > 0) {
464
+ try {
465
+ await environment.kill_process(currentState.browser_pid);
466
+ }
467
+ catch {
468
+ // Ignore cleanup errors for stale local browser processes.
469
+ }
470
+ }
471
+ cleanupOwnedDirectUserDataDir(currentState);
472
+ };
473
+ if (state.cdp_url) {
474
+ try {
475
+ const session = await connectWithState(state);
476
+ return { session, state };
477
+ }
478
+ catch {
479
+ await cleanupDisconnectedState(state);
480
+ clear_direct_state(environment.state_file);
481
+ state = {};
482
+ }
483
+ }
484
+ if (useRemote) {
485
+ const cloudClient = environment.cloud_client_factory?.() ?? new CloudBrowserClient();
486
+ const browser = await cloudClient.create_browser({});
487
+ state = {
488
+ mode: 'remote',
489
+ cdp_url: browser.cdpUrl,
490
+ session_id: browser.id,
491
+ active_url: null,
492
+ };
493
+ save_direct_state(state, environment.state_file);
494
+ return {
495
+ session: await connectWithState(state),
496
+ state,
497
+ };
498
+ }
499
+ const localLaunch = await (environment.local_launcher ?? defaultLocalLauncher)({
500
+ state,
501
+ });
502
+ state = {
503
+ mode: 'local',
504
+ cdp_url: localLaunch.cdp_url,
505
+ browser_pid: localLaunch.browser_pid ?? null,
506
+ user_data_dir: localLaunch.user_data_dir ?? null,
507
+ owns_user_data_dir: localLaunch.owns_user_data_dir ?? null,
508
+ active_url: null,
509
+ };
510
+ save_direct_state(state, environment.state_file);
511
+ return {
512
+ session: await connectWithState(state),
513
+ state,
514
+ };
515
+ };
516
+ const updateDirectStateFromSession = async (session, state, environment) => {
517
+ const currentPage = await session.get_current_page?.();
518
+ const active_url = typeof currentPage?.url === 'function'
519
+ ? String(currentPage.url() ?? '')
520
+ : (session.active_tab?.url ?? null);
521
+ save_direct_state({
522
+ ...state,
523
+ active_url: typeof active_url === 'string' && active_url.trim().length > 0
524
+ ? active_url
525
+ : null,
526
+ }, environment.state_file);
527
+ };
528
+ export const run_direct_command = async (argv, options = {}) => {
529
+ const environment = {
530
+ state_file: options.state_file ?? DIRECT_STATE_FILE,
531
+ stdout: options.stdout ?? DEFAULT_STDOUT,
532
+ stderr: options.stderr ?? DEFAULT_STDERR,
533
+ session_factory: options.session_factory ?? createDefaultSessionFactory(),
534
+ cloud_client_factory: options.cloud_client_factory ?? (() => new CloudBrowserClient()),
535
+ local_launcher: options.local_launcher ?? defaultLocalLauncher,
536
+ kill_process: options.kill_process ??
537
+ ((pid) => {
538
+ process.kill(pid, 'SIGTERM');
539
+ }),
540
+ };
541
+ const { useRemote, args } = extractDirectModeArgs(argv);
542
+ const command = args[0] ?? '';
543
+ if (!command ||
544
+ command === 'help' ||
545
+ command === '--help' ||
546
+ command === '-h') {
547
+ writeLine(environment.stdout, formatDirectUsage());
548
+ return command ? 0 : 1;
549
+ }
550
+ if (command === 'close') {
551
+ const state = load_direct_state(environment.state_file);
552
+ if (!state.cdp_url) {
553
+ writeLine(environment.stdout, 'No active browser session');
554
+ clear_direct_state(environment.state_file);
555
+ return 0;
556
+ }
557
+ if (state.mode === 'remote' && state.session_id) {
558
+ try {
559
+ await environment.cloud_client_factory().stop_browser(state.session_id);
560
+ }
561
+ catch {
562
+ // Best-effort remote cleanup.
563
+ }
564
+ }
565
+ else if (typeof state.browser_pid === 'number' && state.browser_pid > 0) {
566
+ try {
567
+ await environment.kill_process(state.browser_pid);
568
+ }
569
+ catch {
570
+ // Ignore close errors for an already-exited browser.
571
+ }
572
+ }
573
+ cleanupOwnedDirectUserDataDir(state);
574
+ clear_direct_state(environment.state_file);
575
+ writeLine(environment.stdout, 'Browser closed');
576
+ return 0;
577
+ }
578
+ let connected = null;
579
+ try {
580
+ connected = await connectDirectSession(useRemote, environment);
581
+ const { session, state } = connected;
582
+ if (command === 'open') {
583
+ const url = normalizeDirectUrl(args[1] ?? '');
584
+ if (!url) {
585
+ throw new Error('Missing url');
586
+ }
587
+ await session.navigate_to?.(url);
588
+ writeLine(environment.stdout, `Navigated to: ${url}`);
589
+ }
590
+ else if (command === 'state') {
591
+ const summary = await session.get_browser_state_with_recovery?.({
592
+ include_screenshot: false,
593
+ });
594
+ if (!summary) {
595
+ throw new Error('No browser state available');
596
+ }
597
+ const pageInfo = await session.get_page_info?.();
598
+ let output = summary.llm_representation();
599
+ if (pageInfo) {
600
+ output =
601
+ `viewport: ${pageInfo.viewport_width}x${pageInfo.viewport_height}\n` +
602
+ `page: ${pageInfo.page_width}x${pageInfo.page_height}\n` +
603
+ `scroll: (${pageInfo.scroll_x}, ${pageInfo.scroll_y})\n` +
604
+ output;
605
+ }
606
+ writeLine(environment.stdout, output);
607
+ }
608
+ else if (command === 'click') {
609
+ const numericArgs = args.slice(1).map((arg) => Number(arg));
610
+ if (numericArgs.length === 2 && numericArgs.every(Number.isFinite)) {
611
+ const [x, y] = numericArgs;
612
+ await session.click_coordinates?.(x, y);
613
+ writeLine(environment.stdout, `Clicked at (${x}, ${y})`);
614
+ }
615
+ else if (numericArgs.length === 1 &&
616
+ Number.isFinite(numericArgs[0] ?? Number.NaN)) {
617
+ const { node } = await requireDirectNodeByIndex(session, String(numericArgs[0]));
618
+ await session._click_element_node?.(node);
619
+ writeLine(environment.stdout, `Clicked element [${numericArgs[0]}]`);
620
+ }
621
+ else {
622
+ throw new Error('Usage: click <index> or click <x> <y>');
623
+ }
624
+ }
625
+ else if (command === 'type') {
626
+ const text = args.slice(1).join(' ').trim();
627
+ if (!text) {
628
+ throw new Error('Missing text');
629
+ }
630
+ await session.send_keys?.(text);
631
+ writeLine(environment.stdout, `Typed: ${text}`);
632
+ }
633
+ else if (command === 'input') {
634
+ const index = Number(args[1] ?? Number.NaN);
635
+ const text = args.slice(2).join(' ').trim();
636
+ if (!Number.isFinite(index) || !text) {
637
+ throw new Error('Usage: input <index> <text>');
638
+ }
639
+ const { node } = await requireDirectNodeByIndex(session, String(index));
640
+ await session._input_text_element_node?.(node, text, { clear: true });
641
+ writeLine(environment.stdout, `Typed "${text}" into element [${index}]`);
642
+ }
643
+ else if (command === 'screenshot') {
644
+ const outputPath = args[1] ? path.resolve(args[1]) : null;
645
+ const screenshot = await session.take_screenshot?.(false);
646
+ if (!screenshot) {
647
+ throw new Error('Failed to capture screenshot');
648
+ }
649
+ const bytes = Buffer.from(screenshot, 'base64');
650
+ if (outputPath) {
651
+ fs.writeFileSync(outputPath, bytes);
652
+ writeLine(environment.stdout, `Screenshot saved to ${outputPath} (${bytes.length} bytes)`);
653
+ }
654
+ else {
655
+ writeLine(environment.stdout, JSON.stringify({
656
+ screenshot,
657
+ size_bytes: bytes.length,
658
+ }));
659
+ }
660
+ }
661
+ else if (command === 'scroll') {
662
+ const direction = args[1] === 'up' || args[1] === 'left' || args[1] === 'right'
663
+ ? args[1]
664
+ : 'down';
665
+ await session.scroll?.(direction, 500);
666
+ writeLine(environment.stdout, `Scrolled ${direction}`);
667
+ }
668
+ else if (command === 'back') {
669
+ await session.go_back?.();
670
+ writeLine(environment.stdout, 'Navigated back');
671
+ }
672
+ else if (command === 'forward') {
673
+ await session.go_forward?.();
674
+ writeLine(environment.stdout, 'Navigated forward');
675
+ }
676
+ else if (command === 'switch') {
677
+ const rawIdentifier = args[1]?.trim();
678
+ if (!rawIdentifier) {
679
+ throw new Error('Usage: switch <tab>');
680
+ }
681
+ const numericIdentifier = Number(rawIdentifier);
682
+ const identifier = Number.isFinite(numericIdentifier)
683
+ ? numericIdentifier
684
+ : rawIdentifier;
685
+ await session.switch_to_tab?.(identifier);
686
+ writeLine(environment.stdout, `Switched to tab: ${rawIdentifier}`);
687
+ }
688
+ else if (command === 'close-tab') {
689
+ const rawIdentifier = args[1]?.trim();
690
+ const numericIdentifier = rawIdentifier && rawIdentifier.length > 0 ? Number(rawIdentifier) : NaN;
691
+ const identifier = rawIdentifier && rawIdentifier.length > 0
692
+ ? Number.isFinite(numericIdentifier)
693
+ ? numericIdentifier
694
+ : rawIdentifier
695
+ : (session.active_tab?.target_id ?? null);
696
+ if (identifier === null) {
697
+ throw new Error('Usage: close-tab [tab]');
698
+ }
699
+ await session.close_tab?.(identifier);
700
+ writeLine(environment.stdout, `Closed tab: ${identifier}`);
701
+ }
702
+ else if (command === 'keys') {
703
+ const keys = args.slice(1).join(' ').trim();
704
+ if (!keys) {
705
+ throw new Error('Missing keys');
706
+ }
707
+ await session.send_keys?.(keys);
708
+ writeLine(environment.stdout, `Sent keys: ${keys}`);
709
+ }
710
+ else if (command === 'select') {
711
+ const index = args[1];
712
+ const value = args.slice(2).join(' ').trim();
713
+ if (!index || !value) {
714
+ throw new Error('Usage: select <index> <value>');
715
+ }
716
+ const { node, index: numericIndex } = await requireDirectNodeByIndex(session, index);
717
+ await session.select_dropdown_option?.(node, value);
718
+ writeLine(environment.stdout, `Selected "${value}" for element [${numericIndex}]`);
719
+ }
720
+ else if (command === 'wait') {
721
+ const waitCommand = args[1] ?? '';
722
+ if (waitCommand === 'selector') {
723
+ const selector = args[2]?.trim();
724
+ const timeout = Number(args[3] ?? 5000);
725
+ if (!selector) {
726
+ throw new Error('Usage: wait selector <css> [timeout]');
727
+ }
728
+ await session.wait_for_element?.(selector, timeout);
729
+ writeLine(environment.stdout, `Waited for selector "${selector}" (${timeout}ms)`);
730
+ }
731
+ else if (waitCommand === 'text') {
732
+ const text = args.slice(2).join(' ').trim();
733
+ if (!text) {
734
+ throw new Error('Usage: wait text <text>');
735
+ }
736
+ const page = await session.get_current_page?.();
737
+ if (!page?.waitForFunction) {
738
+ throw new Error('No active page available for wait text');
739
+ }
740
+ await page.waitForFunction((needle) => document.body?.innerText?.includes(needle) ?? false, text, { timeout: 5000 });
741
+ writeLine(environment.stdout, `Waited for text "${text}"`);
742
+ }
743
+ else {
744
+ throw new Error('Usage: wait selector <css> | wait text <text>');
745
+ }
746
+ }
747
+ else if (command === 'hover') {
748
+ const { node, index } = await requireDirectNodeByIndex(session, args[1]);
749
+ const locator = await session.get_locate_element?.(node);
750
+ if (!locator?.hover) {
751
+ throw new Error('Hover is not available for this element');
752
+ }
753
+ await locator.hover({ timeout: 5000 });
754
+ writeLine(environment.stdout, `Hovered element [${index}]`);
755
+ }
756
+ else if (command === 'dblclick') {
757
+ const { node, index } = await requireDirectNodeByIndex(session, args[1]);
758
+ const locator = await session.get_locate_element?.(node);
759
+ if (!locator?.dblclick) {
760
+ throw new Error('Double-click is not available for this element');
761
+ }
762
+ await locator.dblclick({ timeout: 5000 });
763
+ writeLine(environment.stdout, `Double-clicked element [${index}]`);
764
+ }
765
+ else if (command === 'rightclick') {
766
+ const { node, index } = await requireDirectNodeByIndex(session, args[1]);
767
+ const locator = await session.get_locate_element?.(node);
768
+ if (!locator?.click) {
769
+ throw new Error('Right-click is not available for this element');
770
+ }
771
+ await locator.click({ button: 'right', timeout: 5000 });
772
+ writeLine(environment.stdout, `Right-clicked element [${index}]`);
773
+ }
774
+ else if (command === 'cookies') {
775
+ const cookieCommand = args[1] ?? '';
776
+ if (cookieCommand === 'get') {
777
+ const parsed = parseDirectCookieOptions(args.slice(2));
778
+ const url = parsed.url ?? parsed.positional[0] ?? null;
779
+ const allCookies = (await session.get_cookies?.()) ?? [];
780
+ const cookies = url
781
+ ? allCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
782
+ : allCookies;
783
+ writeLine(environment.stdout, JSON.stringify({ cookies, count: cookies.length }, null, 2));
784
+ }
785
+ else if (cookieCommand === 'set') {
786
+ if (!session.browser_context?.addCookies) {
787
+ throw new Error('Browser context does not support setting cookies');
788
+ }
789
+ const parsed = parseDirectCookieOptions(args.slice(2));
790
+ const name = parsed.positional[0]?.trim();
791
+ const value = parsed.positional[1] ?? '';
792
+ if (!name || parsed.positional.length < 2) {
793
+ throw new Error('Usage: cookies set <name> <value> [--url <url>] [--domain <domain>] [--path <path>] [--secure] [--http-only] [--same-site <Strict|Lax|None>] [--expires <unix-seconds>]');
794
+ }
795
+ const currentPage = await session.get_current_page?.();
796
+ const currentUrl = typeof currentPage?.url === 'function' ? currentPage.url() : '';
797
+ const cookie = {
798
+ name,
799
+ value,
800
+ path: parsed.path,
801
+ secure: parsed.secure,
802
+ httpOnly: parsed.httpOnly,
803
+ sameSite: parsed.sameSite,
804
+ expires: parsed.expires,
805
+ };
806
+ if (parsed.url) {
807
+ cookie.url = parsed.url;
808
+ }
809
+ else if (parsed.domain) {
810
+ cookie.domain = parsed.domain;
811
+ }
812
+ else if (currentUrl) {
813
+ cookie.url = currentUrl;
814
+ }
815
+ else {
816
+ throw new Error('Provide cookie url/domain or open a page first');
817
+ }
818
+ await session.browser_context.addCookies([cookie]);
819
+ writeLine(environment.stdout, `Set cookie ${name}`);
820
+ }
821
+ else if (cookieCommand === 'clear') {
822
+ if (!session.browser_context?.clearCookies) {
823
+ throw new Error('Browser context does not support clearing cookies');
824
+ }
825
+ const parsed = parseDirectCookieOptions(args.slice(2));
826
+ const url = parsed.url ?? parsed.positional[0] ?? null;
827
+ if (!url) {
828
+ await session.browser_context.clearCookies();
829
+ writeLine(environment.stdout, 'Cleared cookies');
830
+ }
831
+ else {
832
+ const allCookies = (await session.get_cookies?.()) ?? [];
833
+ const remaining = allCookies.filter((cookie) => !cookieMatchesUrl(cookie, url));
834
+ const removedCount = allCookies.length - remaining.length;
835
+ await session.browser_context.clearCookies();
836
+ if (remaining.length > 0 && session.browser_context.addCookies) {
837
+ await session.browser_context.addCookies(remaining);
838
+ }
839
+ writeLine(environment.stdout, `Cleared ${removedCount} cookies matching ${url}`);
840
+ }
841
+ }
842
+ else if (cookieCommand === 'export') {
843
+ const file = args[2]?.trim();
844
+ if (!file) {
845
+ throw new Error('Usage: cookies export <file> [--url <url>]');
846
+ }
847
+ const parsed = parseDirectCookieOptions(args.slice(3));
848
+ const url = parsed.url ?? parsed.positional[0] ?? null;
849
+ const allCookies = (await session.get_cookies?.()) ?? [];
850
+ const cookies = url
851
+ ? allCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
852
+ : allCookies;
853
+ const outputPath = path.resolve(file);
854
+ fs.writeFileSync(outputPath, JSON.stringify(cookies, null, 2), 'utf8');
855
+ writeLine(environment.stdout, `Exported ${cookies.length} cookies to ${outputPath}`);
856
+ }
857
+ else if (cookieCommand === 'import') {
858
+ if (!session.browser_context?.addCookies) {
859
+ throw new Error('Browser context does not support importing cookies');
860
+ }
861
+ const file = args[2]?.trim();
862
+ if (!file) {
863
+ throw new Error('Usage: cookies import <file>');
864
+ }
865
+ const inputPath = path.resolve(file);
866
+ const raw = fs.readFileSync(inputPath, 'utf8');
867
+ const cookies = JSON.parse(raw);
868
+ if (!Array.isArray(cookies)) {
869
+ throw new Error('Cookie import file must contain a JSON array');
870
+ }
871
+ await session.browser_context.addCookies(cookies);
872
+ writeLine(environment.stdout, `Imported ${cookies.length} cookies from ${inputPath}`);
873
+ }
874
+ else {
875
+ throw new Error('Usage: cookies get [url|--url <url>] | cookies set <name> <value> | cookies clear [--url <url>] | cookies export <file> [--url <url>] | cookies import <file>');
876
+ }
877
+ }
878
+ else if (command === 'get') {
879
+ const subcommand = args[1] ?? '';
880
+ if (subcommand === 'title') {
881
+ const page = await session.get_current_page?.();
882
+ if (!page?.title) {
883
+ throw new Error('No active page available for get title');
884
+ }
885
+ writeLine(environment.stdout, await page.title());
886
+ }
887
+ else if (subcommand === 'html') {
888
+ const selector = args.slice(2).join(' ').trim();
889
+ if (!selector) {
890
+ writeLine(environment.stdout, (await session.get_page_html?.()) ?? '');
891
+ }
892
+ else {
893
+ const page = await session.get_current_page?.();
894
+ if (!page?.evaluate) {
895
+ throw new Error('No active page available for get html');
896
+ }
897
+ const html = await page.evaluate((targetSelector) => {
898
+ const element = document.querySelector(targetSelector);
899
+ return element ? element.outerHTML : null;
900
+ }, selector);
901
+ if (typeof html !== 'string' || html.length === 0) {
902
+ throw new Error(`No element found for selector: ${selector}`);
903
+ }
904
+ writeLine(environment.stdout, html);
905
+ }
906
+ }
907
+ else if (subcommand === 'text' ||
908
+ subcommand === 'value' ||
909
+ subcommand === 'attributes' ||
910
+ subcommand === 'bbox') {
911
+ const { node } = await requireDirectNodeByIndex(session, args[2]);
912
+ const value = await readDirectNodeData(session, node, subcommand);
913
+ if (value == null) {
914
+ throw new Error(`Unable to retrieve ${subcommand} for element`);
915
+ }
916
+ writeLine(environment.stdout, typeof value === 'string' ? value : JSON.stringify(value));
917
+ }
918
+ else {
919
+ throw new Error('Usage: get title | get html [selector] | get text <index> | get value <index> | get attributes <index> | get bbox <index>');
920
+ }
921
+ }
922
+ else if (command === 'extract') {
923
+ const query = args.slice(1).join(' ').trim();
924
+ if (!query) {
925
+ throw new Error('Missing query');
926
+ }
927
+ writeLine(environment.stdout, JSON.stringify({
928
+ query,
929
+ error: 'extract requires agent mode - use: browser-use run "extract ..."',
930
+ }));
931
+ }
932
+ else if (command === 'html') {
933
+ const selector = args.slice(1).join(' ').trim();
934
+ if (!selector) {
935
+ writeLine(environment.stdout, (await session.get_page_html?.()) ?? '');
936
+ }
937
+ else {
938
+ const page = await session.get_current_page?.();
939
+ if (!page?.evaluate) {
940
+ throw new Error('No active page available for html');
941
+ }
942
+ const html = await page.evaluate((targetSelector) => {
943
+ const element = document.querySelector(targetSelector);
944
+ return element ? element.outerHTML : null;
945
+ }, selector);
946
+ if (typeof html !== 'string' || html.length === 0) {
947
+ throw new Error(`No element found for selector: ${selector}`);
948
+ }
949
+ writeLine(environment.stdout, html);
950
+ }
951
+ }
952
+ else if (command === 'eval') {
953
+ const script = args.slice(1).join(' ').trim();
954
+ if (!script) {
955
+ throw new Error('Missing js');
956
+ }
957
+ const result = await session.execute_javascript?.(script);
958
+ writeLine(environment.stdout, result === undefined ? 'undefined' : JSON.stringify(result));
959
+ }
960
+ else {
961
+ throw new Error(`Unknown command: ${command}`);
962
+ }
963
+ await updateDirectStateFromSession(session, state, environment);
964
+ await cleanupDirectSession(session);
965
+ return 0;
966
+ }
967
+ catch (error) {
968
+ if (connected?.session) {
969
+ await cleanupDirectSession(connected.session);
970
+ }
971
+ writeLine(environment.stderr, `Error: ${error?.message ?? String(error)}`);
972
+ return 1;
973
+ }
974
+ };
975
+ export const main = async (argv = process.argv.slice(2)) => {
976
+ const exitCode = await run_direct_command(argv);
977
+ if (import.meta.url === `file://${process.argv[1]}`) {
978
+ process.exit(exitCode);
979
+ }
980
+ return exitCode;
981
+ };
982
+ if (import.meta.url === `file://${process.argv[1]}`) {
983
+ void main();
984
+ }