browser-use 0.5.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.
- package/dist/agent/service.js +2 -0
- package/dist/agent/system_prompt.md +269 -0
- package/dist/agent/system_prompt_anthropic_flash.md +240 -0
- package/dist/agent/system_prompt_browser_use.md +18 -0
- package/dist/agent/system_prompt_browser_use_flash.md +15 -0
- package/dist/agent/system_prompt_browser_use_no_thinking.md +17 -0
- package/dist/agent/system_prompt_flash.md +16 -0
- package/dist/agent/system_prompt_flash_anthropic.md +30 -0
- package/dist/agent/system_prompt_no_thinking.md +245 -0
- package/dist/browser/cloud/index.d.ts +1 -0
- package/dist/browser/cloud/index.js +1 -0
- package/dist/browser/cloud/management.d.ts +130 -0
- package/dist/browser/cloud/management.js +140 -0
- package/dist/browser/events.d.ts +61 -3
- package/dist/browser/events.js +66 -0
- package/dist/browser/profile.d.ts +1 -0
- package/dist/browser/profile.js +1 -0
- package/dist/browser/session.d.ts +56 -2
- package/dist/browser/session.js +596 -24
- package/dist/browser/watchdogs/base.js +34 -1
- package/dist/browser/watchdogs/captcha-watchdog.d.ts +26 -0
- package/dist/browser/watchdogs/captcha-watchdog.js +151 -0
- package/dist/browser/watchdogs/index.d.ts +1 -0
- package/dist/browser/watchdogs/index.js +1 -0
- package/dist/browser/watchdogs/screenshot-watchdog.js +4 -3
- package/dist/cli.d.ts +120 -0
- package/dist/cli.js +1816 -4
- package/dist/controller/service.js +106 -362
- package/dist/controller/views.d.ts +9 -6
- package/dist/controller/views.js +8 -5
- package/dist/filesystem/file-system.js +1 -1
- package/dist/llm/litellm/chat.d.ts +11 -0
- package/dist/llm/litellm/chat.js +16 -0
- package/dist/llm/litellm/index.d.ts +1 -0
- package/dist/llm/litellm/index.js +1 -0
- package/dist/llm/models.js +29 -3
- package/dist/llm/oci-raw/chat.d.ts +64 -0
- package/dist/llm/oci-raw/chat.js +350 -0
- package/dist/llm/oci-raw/index.d.ts +2 -0
- package/dist/llm/oci-raw/index.js +2 -0
- package/dist/llm/oci-raw/serializer.d.ts +12 -0
- package/dist/llm/oci-raw/serializer.js +128 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +62 -13
- package/dist/skill-cli/direct.d.ts +100 -0
- package/dist/skill-cli/direct.js +984 -0
- package/dist/skill-cli/index.d.ts +2 -0
- package/dist/skill-cli/index.js +2 -0
- package/dist/skill-cli/server.d.ts +2 -0
- package/dist/skill-cli/server.js +472 -11
- package/dist/skill-cli/tunnel.d.ts +61 -0
- package/dist/skill-cli/tunnel.js +257 -0
- package/dist/sync/auth.d.ts +8 -0
- package/dist/sync/auth.js +12 -0
- 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
|
+
}
|