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.
- 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 +25 -8
- package/dist/browser/session.d.ts +59 -2
- package/dist/browser/session.js +943 -131
- 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/dom/dom_tree/index.js +24 -11
- 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/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -1
- package/package.json +22 -4
package/dist/skill-cli/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export interface SkillCliServerOptions {
|
|
|
6
6
|
export declare class SkillCliServer {
|
|
7
7
|
readonly registry: SessionRegistry;
|
|
8
8
|
constructor(options?: SkillCliServerOptions);
|
|
9
|
+
private _require_node_by_index;
|
|
10
|
+
private _read_node_data;
|
|
9
11
|
private _handle_browser_action;
|
|
10
12
|
handle_request(request: Request | string): Promise<Response>;
|
|
11
13
|
}
|
package/dist/skill-cli/server.js
CHANGED
|
@@ -1,51 +1,512 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { Request, Response } from './protocol.js';
|
|
2
4
|
import { SessionRegistry } from './sessions.js';
|
|
5
|
+
const normalizeCookieDomain = (value) => String(value ?? '')
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/^\./, '')
|
|
8
|
+
.toLowerCase();
|
|
9
|
+
const parseCookieHostname = (url) => {
|
|
10
|
+
const value = String(url ?? '').trim();
|
|
11
|
+
if (!value) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return new URL(value).hostname.toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const parseCookieUrl = (url) => {
|
|
22
|
+
const value = String(url ?? '').trim();
|
|
23
|
+
if (!value) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return new URL(value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const cookiePathMatches = (cookiePath, urlPath) => {
|
|
34
|
+
const normalizedCookiePath = typeof cookiePath === 'string' && cookiePath.length > 0 ? cookiePath : '/';
|
|
35
|
+
if (normalizedCookiePath === '/') {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (urlPath === normalizedCookiePath) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return urlPath.startsWith(normalizedCookiePath.endsWith('/')
|
|
42
|
+
? normalizedCookiePath
|
|
43
|
+
: `${normalizedCookiePath}/`);
|
|
44
|
+
};
|
|
45
|
+
const cookieMatchesUrl = (cookie, url) => {
|
|
46
|
+
const parsedUrl = parseCookieUrl(url);
|
|
47
|
+
const hostname = parsedUrl?.hostname.toLowerCase() ?? '';
|
|
48
|
+
const domain = normalizeCookieDomain(cookie.domain);
|
|
49
|
+
if (!hostname || !domain) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (!(hostname === domain ||
|
|
53
|
+
hostname.endsWith(`.${domain}`) ||
|
|
54
|
+
domain.endsWith(`.${hostname}`))) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!cookiePathMatches(cookie.path, parsedUrl?.pathname || '/')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (cookie.secure && parsedUrl?.protocol !== 'https:') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
const normalizeSameSite = (value) => {
|
|
66
|
+
const normalized = String(value ?? '')
|
|
67
|
+
.trim()
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
if (normalized === 'strict') {
|
|
70
|
+
return 'Strict';
|
|
71
|
+
}
|
|
72
|
+
if (normalized === 'lax') {
|
|
73
|
+
return 'Lax';
|
|
74
|
+
}
|
|
75
|
+
if (normalized === 'none') {
|
|
76
|
+
return 'None';
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
const parseCookieExpires = (value) => {
|
|
81
|
+
if (value == null || value === '') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const parsed = Number(value);
|
|
85
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
86
|
+
};
|
|
3
87
|
export class SkillCliServer {
|
|
4
88
|
registry;
|
|
5
89
|
constructor(options = {}) {
|
|
6
90
|
this.registry = options.registry ?? new SessionRegistry();
|
|
7
91
|
}
|
|
92
|
+
async _require_node_by_index(browser_session, index) {
|
|
93
|
+
const parsedIndex = Number(index);
|
|
94
|
+
if (!Number.isFinite(parsedIndex)) {
|
|
95
|
+
throw new Error('Missing index');
|
|
96
|
+
}
|
|
97
|
+
const node = await browser_session.get_dom_element_by_index(parsedIndex);
|
|
98
|
+
if (!node) {
|
|
99
|
+
return {
|
|
100
|
+
error: `Element index ${parsedIndex} not found - page may have changed`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return node;
|
|
104
|
+
}
|
|
105
|
+
async _read_node_data(browser_session, node, kind) {
|
|
106
|
+
if (!node?.xpath) {
|
|
107
|
+
throw new Error('DOM element does not include an XPath selector');
|
|
108
|
+
}
|
|
109
|
+
const page = await browser_session.get_current_page();
|
|
110
|
+
if (!page?.evaluate) {
|
|
111
|
+
throw new Error('No active page available');
|
|
112
|
+
}
|
|
113
|
+
return await page.evaluate(({ xpath, dataKind }) => {
|
|
114
|
+
const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
115
|
+
if (!element) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (dataKind === 'text') {
|
|
119
|
+
return element.textContent?.trim() ?? '';
|
|
120
|
+
}
|
|
121
|
+
if (dataKind === 'value') {
|
|
122
|
+
return 'value' in element
|
|
123
|
+
? String(element.value ?? '')
|
|
124
|
+
: null;
|
|
125
|
+
}
|
|
126
|
+
if (dataKind === 'attributes') {
|
|
127
|
+
return Object.fromEntries(Array.from(element.attributes).map((attribute) => [
|
|
128
|
+
attribute.name,
|
|
129
|
+
attribute.value,
|
|
130
|
+
]));
|
|
131
|
+
}
|
|
132
|
+
if (dataKind === 'bbox') {
|
|
133
|
+
const rect = element.getBoundingClientRect();
|
|
134
|
+
return {
|
|
135
|
+
x: rect.x,
|
|
136
|
+
y: rect.y,
|
|
137
|
+
width: rect.width,
|
|
138
|
+
height: rect.height,
|
|
139
|
+
top: rect.top,
|
|
140
|
+
right: rect.right,
|
|
141
|
+
bottom: rect.bottom,
|
|
142
|
+
left: rect.left,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}, { xpath: node.xpath, dataKind: kind });
|
|
147
|
+
}
|
|
8
148
|
async _handle_browser_action(action, sessionName, params) {
|
|
9
149
|
const session = await this.registry.get_or_create_session(sessionName);
|
|
10
150
|
const browser_session = session.browser_session;
|
|
11
151
|
if (action === 'open') {
|
|
12
|
-
|
|
152
|
+
let url = String(params.url ?? '').trim();
|
|
13
153
|
if (!url) {
|
|
14
154
|
throw new Error('Missing url');
|
|
15
155
|
}
|
|
156
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
|
|
157
|
+
url = `https://${url}`;
|
|
158
|
+
}
|
|
16
159
|
await browser_session.navigate_to(url);
|
|
17
160
|
return { url };
|
|
18
161
|
}
|
|
19
162
|
if (action === 'click') {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
const node = await browser_session.get_dom_element_by_index(index);
|
|
25
|
-
if (!node) {
|
|
26
|
-
return {
|
|
27
|
-
error: `Element index ${index} not found - page may have changed`,
|
|
28
|
-
};
|
|
163
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
164
|
+
if ('error' in node) {
|
|
165
|
+
return node;
|
|
29
166
|
}
|
|
30
167
|
await browser_session._click_element_node(node);
|
|
31
|
-
return { clicked: index };
|
|
168
|
+
return { clicked: Number(params.index) };
|
|
169
|
+
}
|
|
170
|
+
if (action === 'hover') {
|
|
171
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
172
|
+
if ('error' in node) {
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
const locator = await browser_session.get_locate_element(node);
|
|
176
|
+
if (!locator?.hover) {
|
|
177
|
+
throw new Error('Hover is not available for this element');
|
|
178
|
+
}
|
|
179
|
+
await locator.hover({ timeout: 5000 });
|
|
180
|
+
return { hovered: Number(params.index) };
|
|
181
|
+
}
|
|
182
|
+
if (action === 'dblclick') {
|
|
183
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
184
|
+
if ('error' in node) {
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
const locator = await browser_session.get_locate_element(node);
|
|
188
|
+
if (!locator?.dblclick) {
|
|
189
|
+
throw new Error('Double-click is not available for this element');
|
|
190
|
+
}
|
|
191
|
+
await locator.dblclick({ timeout: 5000 });
|
|
192
|
+
return { double_clicked: Number(params.index) };
|
|
193
|
+
}
|
|
194
|
+
if (action === 'rightclick') {
|
|
195
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
196
|
+
if ('error' in node) {
|
|
197
|
+
return node;
|
|
198
|
+
}
|
|
199
|
+
const locator = await browser_session.get_locate_element(node);
|
|
200
|
+
if (!locator?.click) {
|
|
201
|
+
throw new Error('Right-click is not available for this element');
|
|
202
|
+
}
|
|
203
|
+
await locator.click({ button: 'right', timeout: 5000 });
|
|
204
|
+
return { right_clicked: Number(params.index) };
|
|
32
205
|
}
|
|
33
206
|
if (action === 'type') {
|
|
34
207
|
const text = String(params.text ?? '');
|
|
35
208
|
await browser_session.send_keys(text);
|
|
36
209
|
return { typed: text };
|
|
37
210
|
}
|
|
211
|
+
if (action === 'input') {
|
|
212
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
213
|
+
if ('error' in node) {
|
|
214
|
+
return node;
|
|
215
|
+
}
|
|
216
|
+
const text = String(params.text ?? '');
|
|
217
|
+
const clear = typeof params.clear === 'boolean' ? params.clear : true;
|
|
218
|
+
await browser_session._input_text_element_node(node, text, { clear });
|
|
219
|
+
return { input: Number(params.index), text, clear };
|
|
220
|
+
}
|
|
38
221
|
if (action === 'state') {
|
|
39
222
|
const state = await browser_session.get_browser_state_with_recovery({
|
|
40
223
|
include_screenshot: false,
|
|
41
224
|
});
|
|
225
|
+
const page_info = typeof browser_session.get_page_info === 'function'
|
|
226
|
+
? await browser_session.get_page_info()
|
|
227
|
+
: null;
|
|
42
228
|
return {
|
|
43
229
|
url: state.url,
|
|
44
230
|
title: state.title,
|
|
45
231
|
tabs: state.tabs,
|
|
232
|
+
page_info,
|
|
46
233
|
llm_representation: state.llm_representation(),
|
|
47
234
|
};
|
|
48
235
|
}
|
|
236
|
+
if (action === 'screenshot') {
|
|
237
|
+
const screenshot = await browser_session.take_screenshot(Boolean(params.full));
|
|
238
|
+
if (!screenshot) {
|
|
239
|
+
throw new Error('Failed to capture screenshot');
|
|
240
|
+
}
|
|
241
|
+
const file = typeof params.file === 'string' ? params.file.trim() : '';
|
|
242
|
+
if (!file) {
|
|
243
|
+
return { screenshot };
|
|
244
|
+
}
|
|
245
|
+
const filePath = path.resolve(file);
|
|
246
|
+
await fsp.writeFile(filePath, Buffer.from(screenshot, 'base64'));
|
|
247
|
+
return { file: filePath };
|
|
248
|
+
}
|
|
249
|
+
if (action === 'wait_selector') {
|
|
250
|
+
const selector = String(params.selector ?? '');
|
|
251
|
+
if (!selector) {
|
|
252
|
+
throw new Error('Missing selector');
|
|
253
|
+
}
|
|
254
|
+
const timeout = Number(params.timeout ?? 5000);
|
|
255
|
+
await browser_session.wait_for_element(selector, timeout);
|
|
256
|
+
return { waited_for: 'selector', selector, timeout };
|
|
257
|
+
}
|
|
258
|
+
if (action === 'wait_text') {
|
|
259
|
+
const text = String(params.text ?? '');
|
|
260
|
+
if (!text) {
|
|
261
|
+
throw new Error('Missing text');
|
|
262
|
+
}
|
|
263
|
+
const timeout = Number(params.timeout ?? 5000);
|
|
264
|
+
const page = await browser_session.get_current_page();
|
|
265
|
+
if (!page?.waitForFunction) {
|
|
266
|
+
throw new Error('No active page available for wait_text');
|
|
267
|
+
}
|
|
268
|
+
await page.waitForFunction((needle) => document.body?.innerText?.includes(needle) ?? false, text, { timeout });
|
|
269
|
+
return { waited_for: 'text', text, timeout };
|
|
270
|
+
}
|
|
271
|
+
if (action === 'scroll') {
|
|
272
|
+
let direction = 'down';
|
|
273
|
+
if (typeof params.direction === 'string' &&
|
|
274
|
+
['up', 'down', 'left', 'right'].includes(params.direction)) {
|
|
275
|
+
direction = params.direction;
|
|
276
|
+
}
|
|
277
|
+
const amount = Number(params.amount ?? 500);
|
|
278
|
+
await browser_session.scroll(direction, amount);
|
|
279
|
+
return { direction, amount };
|
|
280
|
+
}
|
|
281
|
+
if (action === 'back') {
|
|
282
|
+
await browser_session.go_back();
|
|
283
|
+
return { navigated: 'back' };
|
|
284
|
+
}
|
|
285
|
+
if (action === 'forward') {
|
|
286
|
+
await browser_session.go_forward();
|
|
287
|
+
return { navigated: 'forward' };
|
|
288
|
+
}
|
|
289
|
+
if (action === 'switch') {
|
|
290
|
+
const identifier = params.tab ?? params.target_id;
|
|
291
|
+
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
|
|
292
|
+
throw new Error('Missing tab');
|
|
293
|
+
}
|
|
294
|
+
await browser_session.switch_to_tab(identifier);
|
|
295
|
+
return {
|
|
296
|
+
active_tab: browser_session.active_tab?.target_id ??
|
|
297
|
+
browser_session.active_tab?.tab_id ??
|
|
298
|
+
browser_session.active_tab?.page_id ??
|
|
299
|
+
null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (action === 'close_tab' || action === 'close-tab') {
|
|
303
|
+
const identifier = params.tab ??
|
|
304
|
+
params.target_id ??
|
|
305
|
+
browser_session.active_tab?.target_id ??
|
|
306
|
+
browser_session.active_tab?.page_id ??
|
|
307
|
+
browser_session.active_tab_index;
|
|
308
|
+
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
|
|
309
|
+
throw new Error('Missing tab');
|
|
310
|
+
}
|
|
311
|
+
await browser_session.close_tab(identifier);
|
|
312
|
+
return { closed_tab: identifier };
|
|
313
|
+
}
|
|
314
|
+
if (action === 'keys') {
|
|
315
|
+
const keys = String(params.keys ?? '');
|
|
316
|
+
if (!keys) {
|
|
317
|
+
throw new Error('Missing keys');
|
|
318
|
+
}
|
|
319
|
+
await browser_session.send_keys(keys);
|
|
320
|
+
return { keys };
|
|
321
|
+
}
|
|
322
|
+
if (action === 'select') {
|
|
323
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
324
|
+
if ('error' in node) {
|
|
325
|
+
return node;
|
|
326
|
+
}
|
|
327
|
+
const value = String(params.value ?? '');
|
|
328
|
+
if (!value) {
|
|
329
|
+
throw new Error('Missing value');
|
|
330
|
+
}
|
|
331
|
+
const selected = await browser_session.select_dropdown_option(node, value);
|
|
332
|
+
return {
|
|
333
|
+
index: Number(params.index),
|
|
334
|
+
value,
|
|
335
|
+
selected,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (action === 'html') {
|
|
339
|
+
const selector = typeof params.selector === 'string' ? params.selector.trim() : '';
|
|
340
|
+
if (!selector) {
|
|
341
|
+
return { html: await browser_session.get_page_html() };
|
|
342
|
+
}
|
|
343
|
+
const page = await browser_session.get_current_page();
|
|
344
|
+
if (!page?.evaluate) {
|
|
345
|
+
throw new Error('No active page available for html');
|
|
346
|
+
}
|
|
347
|
+
const html = await page.evaluate((targetSelector) => {
|
|
348
|
+
const element = document.querySelector(targetSelector);
|
|
349
|
+
return element ? element.outerHTML : null;
|
|
350
|
+
}, selector);
|
|
351
|
+
if (typeof html !== 'string' || html.length === 0) {
|
|
352
|
+
throw new Error(`No element found for selector: ${selector}`);
|
|
353
|
+
}
|
|
354
|
+
return { selector, html };
|
|
355
|
+
}
|
|
356
|
+
if (action === 'eval') {
|
|
357
|
+
const script = String(params.js ?? params.script ?? '').trim();
|
|
358
|
+
if (!script) {
|
|
359
|
+
throw new Error('Missing js');
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
result: await browser_session.execute_javascript(script),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (action === 'extract') {
|
|
366
|
+
const query = String(params.query ?? '').trim();
|
|
367
|
+
if (!query) {
|
|
368
|
+
throw new Error('Missing query');
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
query,
|
|
372
|
+
error: 'extract requires agent mode - use: browser-use run "extract ..."',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (action === 'get_title') {
|
|
376
|
+
const page = await browser_session.get_current_page?.();
|
|
377
|
+
if (!page?.title) {
|
|
378
|
+
throw new Error('No active page available for get_title');
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
title: await page.title(),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (action === 'get_html') {
|
|
385
|
+
const selector = typeof params.selector === 'string' ? params.selector.trim() : '';
|
|
386
|
+
return selector
|
|
387
|
+
? await this._handle_browser_action('html', sessionName, { selector })
|
|
388
|
+
: await this._handle_browser_action('html', sessionName, {});
|
|
389
|
+
}
|
|
390
|
+
if (action === 'get_text' ||
|
|
391
|
+
action === 'get_value' ||
|
|
392
|
+
action === 'get_attributes' ||
|
|
393
|
+
action === 'get_bbox') {
|
|
394
|
+
const node = await this._require_node_by_index(browser_session, params.index);
|
|
395
|
+
if ('error' in node) {
|
|
396
|
+
return node;
|
|
397
|
+
}
|
|
398
|
+
const kind = action.replace('get_', '');
|
|
399
|
+
const value = await this._read_node_data(browser_session, node, kind);
|
|
400
|
+
if (value == null) {
|
|
401
|
+
throw new Error(`Unable to retrieve ${kind} for element`);
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
index: Number(params.index),
|
|
405
|
+
[kind]: value,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (action === 'cookies_get') {
|
|
409
|
+
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
410
|
+
const allCookies = (await browser_session.get_cookies());
|
|
411
|
+
const cookies = url
|
|
412
|
+
? allCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
|
|
413
|
+
: allCookies;
|
|
414
|
+
return { cookies, count: cookies.length };
|
|
415
|
+
}
|
|
416
|
+
if (action === 'cookies_set') {
|
|
417
|
+
const name = String(params.name ?? '').trim();
|
|
418
|
+
const value = String(params.value ?? '');
|
|
419
|
+
if (!name) {
|
|
420
|
+
throw new Error('Missing cookie name');
|
|
421
|
+
}
|
|
422
|
+
if (!browser_session.browser_context?.addCookies) {
|
|
423
|
+
throw new Error('Browser context does not support setting cookies');
|
|
424
|
+
}
|
|
425
|
+
const currentPage = await browser_session.get_current_page?.();
|
|
426
|
+
const currentUrl = typeof currentPage?.url === 'function' ? currentPage.url() : '';
|
|
427
|
+
const cookie = {
|
|
428
|
+
name,
|
|
429
|
+
value,
|
|
430
|
+
url: typeof params.url === 'string' && params.url.trim().length > 0
|
|
431
|
+
? params.url.trim()
|
|
432
|
+
: undefined,
|
|
433
|
+
domain: typeof params.domain === 'string' ? params.domain.trim() : undefined,
|
|
434
|
+
path: typeof params.path === 'string' ? params.path : '/',
|
|
435
|
+
secure: Boolean(params.secure),
|
|
436
|
+
httpOnly: Boolean(params.http_only),
|
|
437
|
+
sameSite: normalizeSameSite(params.same_site ?? params.sameSite),
|
|
438
|
+
expires: parseCookieExpires(params.expires),
|
|
439
|
+
};
|
|
440
|
+
if (!cookie.url && !cookie.domain && currentUrl) {
|
|
441
|
+
cookie.url = currentUrl;
|
|
442
|
+
}
|
|
443
|
+
if (!cookie.url && !cookie.domain) {
|
|
444
|
+
throw new Error('Provide cookie url/domain or open a page first');
|
|
445
|
+
}
|
|
446
|
+
await browser_session.browser_context.addCookies([cookie]);
|
|
447
|
+
return { set: name };
|
|
448
|
+
}
|
|
449
|
+
if (action === 'cookies_clear') {
|
|
450
|
+
if (!browser_session.browser_context?.clearCookies) {
|
|
451
|
+
throw new Error('Browser context does not support clearing cookies');
|
|
452
|
+
}
|
|
453
|
+
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
454
|
+
if (!url) {
|
|
455
|
+
await browser_session.browser_context.clearCookies();
|
|
456
|
+
return { cleared: true };
|
|
457
|
+
}
|
|
458
|
+
const allCookies = (await browser_session.get_cookies());
|
|
459
|
+
const remainingCookies = allCookies.filter((cookie) => !cookieMatchesUrl(cookie, url));
|
|
460
|
+
const removedCount = allCookies.length - remainingCookies.length;
|
|
461
|
+
await browser_session.browser_context.clearCookies();
|
|
462
|
+
if (remainingCookies.length > 0 &&
|
|
463
|
+
browser_session.browser_context.addCookies) {
|
|
464
|
+
await browser_session.browser_context.addCookies(remainingCookies);
|
|
465
|
+
}
|
|
466
|
+
return { cleared: true, url, count: removedCount };
|
|
467
|
+
}
|
|
468
|
+
if (action === 'cookies_export') {
|
|
469
|
+
const file = String(params.file ?? '').trim();
|
|
470
|
+
if (!file) {
|
|
471
|
+
throw new Error('Missing file');
|
|
472
|
+
}
|
|
473
|
+
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
474
|
+
const allCookies = (await browser_session.get_cookies());
|
|
475
|
+
const cookies = url
|
|
476
|
+
? allCookies.filter((cookie) => cookieMatchesUrl(cookie, url))
|
|
477
|
+
: allCookies;
|
|
478
|
+
const filePath = path.resolve(file);
|
|
479
|
+
await fsp.writeFile(filePath, JSON.stringify(cookies, null, 2));
|
|
480
|
+
return { file: filePath, count: cookies.length };
|
|
481
|
+
}
|
|
482
|
+
if (action === 'cookies_import') {
|
|
483
|
+
const file = String(params.file ?? '').trim();
|
|
484
|
+
if (!file) {
|
|
485
|
+
throw new Error('Missing file');
|
|
486
|
+
}
|
|
487
|
+
if (!browser_session.browser_context?.addCookies) {
|
|
488
|
+
throw new Error('Browser context does not support importing cookies');
|
|
489
|
+
}
|
|
490
|
+
const filePath = path.resolve(file);
|
|
491
|
+
const raw = await fsp.readFile(filePath, 'utf8');
|
|
492
|
+
const cookies = JSON.parse(raw);
|
|
493
|
+
if (!Array.isArray(cookies)) {
|
|
494
|
+
throw new Error('Cookie import file must contain a JSON array');
|
|
495
|
+
}
|
|
496
|
+
const importedCookies = cookies.map((cookie) => {
|
|
497
|
+
if (!cookie || typeof cookie !== 'object') {
|
|
498
|
+
throw new Error('Each imported cookie must be a JSON object');
|
|
499
|
+
}
|
|
500
|
+
const typedCookie = cookie;
|
|
501
|
+
if (typeof typedCookie.name !== 'string' ||
|
|
502
|
+
typeof typedCookie.value !== 'string') {
|
|
503
|
+
throw new Error('Each imported cookie must include string name/value');
|
|
504
|
+
}
|
|
505
|
+
return typedCookie;
|
|
506
|
+
});
|
|
507
|
+
await browser_session.browser_context.addCookies(importedCookies);
|
|
508
|
+
return { file: filePath, imported: importedCookies.length };
|
|
509
|
+
}
|
|
49
510
|
if (action === 'close') {
|
|
50
511
|
await this.registry.close_session(sessionName);
|
|
51
512
|
return { closed: sessionName };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export type TunnelStatus = {
|
|
3
|
+
available: boolean;
|
|
4
|
+
source: 'system' | null;
|
|
5
|
+
path: string | null;
|
|
6
|
+
note: string;
|
|
7
|
+
};
|
|
8
|
+
export type StartTunnelResult = {
|
|
9
|
+
url: string;
|
|
10
|
+
port: number;
|
|
11
|
+
existing?: boolean;
|
|
12
|
+
} | {
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
export type ListTunnelsResult = {
|
|
16
|
+
tunnels: Array<{
|
|
17
|
+
port: number;
|
|
18
|
+
url: string;
|
|
19
|
+
}>;
|
|
20
|
+
count: number;
|
|
21
|
+
};
|
|
22
|
+
export type StopTunnelResult = {
|
|
23
|
+
stopped: number;
|
|
24
|
+
url: string;
|
|
25
|
+
} | {
|
|
26
|
+
error: string;
|
|
27
|
+
};
|
|
28
|
+
export type StopAllTunnelsResult = {
|
|
29
|
+
stopped: number[];
|
|
30
|
+
count: number;
|
|
31
|
+
};
|
|
32
|
+
export interface TunnelManagerOptions {
|
|
33
|
+
tunnel_dir?: string;
|
|
34
|
+
binary_resolver?: (binary: string) => string | null;
|
|
35
|
+
spawn_impl?: typeof spawn;
|
|
36
|
+
sleep_impl?: (ms: number) => Promise<void>;
|
|
37
|
+
is_process_alive?: (pid: number) => boolean;
|
|
38
|
+
kill_process?: (pid: number) => Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
export declare class TunnelManager {
|
|
41
|
+
private readonly tunnel_dir;
|
|
42
|
+
private readonly binary_resolver;
|
|
43
|
+
private readonly spawn_impl;
|
|
44
|
+
private readonly sleep_impl;
|
|
45
|
+
private readonly is_process_alive_impl;
|
|
46
|
+
private readonly kill_process_impl;
|
|
47
|
+
private binary_path;
|
|
48
|
+
constructor(options?: TunnelManagerOptions);
|
|
49
|
+
private get_tunnel_file;
|
|
50
|
+
private get_tunnel_log_file;
|
|
51
|
+
private save_tunnel_info;
|
|
52
|
+
private load_tunnel_info;
|
|
53
|
+
get_binary_path(): string;
|
|
54
|
+
is_available(): boolean;
|
|
55
|
+
get_status(): TunnelStatus;
|
|
56
|
+
start_tunnel(port: number): Promise<StartTunnelResult>;
|
|
57
|
+
list_tunnels(): ListTunnelsResult;
|
|
58
|
+
stop_tunnel(port: number): Promise<StopTunnelResult>;
|
|
59
|
+
stop_all_tunnels(): Promise<StopAllTunnelsResult>;
|
|
60
|
+
}
|
|
61
|
+
export declare const get_tunnel_manager: () => TunnelManager;
|