brave-real-browser-mcp-server 2.15.5 → 2.15.7
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/brave-installer.js +182 -0
- package/dist/browser-manager.js +22 -1
- package/dist/handlers/data-extraction-handlers.js +33 -3
- package/dist/handlers/multi-element-handlers.js +5 -67
- package/dist/handlers/navigation-handlers.js +59 -0
- package/dist/handlers/search-filter-handlers.js +0 -121
- package/dist/handlers/smart-data-extractors.js +21 -1
- package/dist/index.js +20 -62
- package/dist/tool-definitions.js +6 -218
- package/package.json +2 -2
- package/scripts/check-tool-registration.ts +66 -0
- package/scripts/full-verification.ts +98 -0
- package/scripts/live-verification.ts +61 -0
- package/scripts/verify-brave-installer.cjs +13 -0
- package/scripts/verify-fixes-custom.ts +108 -0
- package/scripts/verify-fixes-standalone.js +244 -0
- package/scripts/verify-fixes-standalone.ts +248 -0
- package/dist/handlers/data-processing-handlers.js +0 -49
- package/dist/handlers/pagination-handlers.js +0 -115
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { exec, spawn } from 'child_process';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export class BraveInstaller {
|
|
9
|
+
static WINDOWS_INSTALLER_URL = 'https://laptop-updates.brave.com/latest/winx64';
|
|
10
|
+
static MAC_INSTALLER_URL = 'https://laptop-updates.brave.com/latest/osx';
|
|
11
|
+
/**
|
|
12
|
+
* Install Brave Browser based on the current platform with Silent/Auto options
|
|
13
|
+
*/
|
|
14
|
+
static async install() {
|
|
15
|
+
const platform = process.platform;
|
|
16
|
+
console.error(`⬇️ Attempting to install Brave Browser for ${platform} (Silent Mode)...`);
|
|
17
|
+
try {
|
|
18
|
+
if (platform === 'win32') {
|
|
19
|
+
return await this.installOnWindows();
|
|
20
|
+
}
|
|
21
|
+
else if (platform === 'darwin') {
|
|
22
|
+
return await this.installOnMac();
|
|
23
|
+
}
|
|
24
|
+
else if (platform === 'linux') {
|
|
25
|
+
return await this.installOnLinux();
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(`❌ Failed to install Brave: ${error.message}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Install Brave on Windows (Silent Install)
|
|
36
|
+
*/
|
|
37
|
+
static async installOnWindows() {
|
|
38
|
+
const tempDir = os.tmpdir();
|
|
39
|
+
const installerPath = path.join(tempDir, 'BraveBrowserSetup.exe');
|
|
40
|
+
console.error('1️⃣ Downloading Brave Installer...');
|
|
41
|
+
try {
|
|
42
|
+
await this.downloadFile(this.WINDOWS_INSTALLER_URL, installerPath);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.error(` Download failed: ${e.message}`);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
console.error('2️⃣ Running Installer (Silent Mode)...');
|
|
49
|
+
try {
|
|
50
|
+
// /silent and /install are standard for Brave/Chrome
|
|
51
|
+
// Triggers UAC prompt if not already elevated, but handles the UI silently
|
|
52
|
+
const installCmd = `"${installerPath}" --silent --install`;
|
|
53
|
+
await execAsync(installCmd);
|
|
54
|
+
console.error('⏳ Waiting for installation to complete...');
|
|
55
|
+
// Wait multiple intervals to ensure filesystem sync
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error(' Silent install failed, attempting interactive fallback...');
|
|
61
|
+
try {
|
|
62
|
+
// Fallback: Launch normally so user can interact
|
|
63
|
+
const child = spawn(installerPath, { detached: true, stdio: 'ignore' });
|
|
64
|
+
child.unref();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.error(` Failed to launch installer: ${error.message}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Install Brave on Mac (Silent/Automated Install via DMG)
|
|
75
|
+
*/
|
|
76
|
+
static async installOnMac() {
|
|
77
|
+
const tempDir = os.tmpdir();
|
|
78
|
+
const dmgPath = path.join(tempDir, 'Brave-Browser.dmg');
|
|
79
|
+
console.error('1️⃣ Downloading Brave DMG...');
|
|
80
|
+
try {
|
|
81
|
+
await this.downloadFile(this.MAC_INSTALLER_URL, dmgPath);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error(` Download failed: ${e.message}`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
console.error('2️⃣ Mounting DMG...');
|
|
88
|
+
try {
|
|
89
|
+
// Attach huge image quietly
|
|
90
|
+
await execAsync(`hdiutil attach "${dmgPath}" -nobrowse -quiet`);
|
|
91
|
+
console.error('3️⃣ Copying to Applications...');
|
|
92
|
+
// Copy the app bundle
|
|
93
|
+
try {
|
|
94
|
+
await execAsync(`cp -R "/Volumes/Brave Browser/Brave Browser.app" /Applications/`);
|
|
95
|
+
// Cleanup: Detach
|
|
96
|
+
try {
|
|
97
|
+
await execAsync(`hdiutil detach "/Volumes/Brave Browser" -quiet`);
|
|
98
|
+
}
|
|
99
|
+
catch (e) { }
|
|
100
|
+
// Force attribute update to remove quarantine (avoids 'downloaded from internet' popup)
|
|
101
|
+
try {
|
|
102
|
+
await execAsync(`xattr -r -d com.apple.quarantine "/Applications/Brave Browser.app"`);
|
|
103
|
+
}
|
|
104
|
+
catch (e) { }
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(' Copy failed (Permission denied?). Opening DMG for user manually...');
|
|
109
|
+
await execAsync(`open "${dmgPath}"`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(` Install failed: ${error.message}`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Install Brave on Linux (Debian/Ubuntu/Fedora support)
|
|
120
|
+
*/
|
|
121
|
+
static async installOnLinux() {
|
|
122
|
+
console.error('1️⃣ Detecting Linux distribution...');
|
|
123
|
+
try {
|
|
124
|
+
let dist = 'unknown';
|
|
125
|
+
if (fs.existsSync('/etc/debian_version'))
|
|
126
|
+
dist = 'debian';
|
|
127
|
+
else if (fs.existsSync('/etc/fedora-release'))
|
|
128
|
+
dist = 'fedora';
|
|
129
|
+
// Can extend for Arch/openSUSE if needed
|
|
130
|
+
console.error(` Detected family: ${dist}`);
|
|
131
|
+
if (dist === 'debian') {
|
|
132
|
+
console.error('2️⃣ Installing for Debian/Ubuntu (Attempting sudo)...');
|
|
133
|
+
// Basic check if we are root or can sudo
|
|
134
|
+
const cmd = `
|
|
135
|
+
sudo apt install -y curl &&
|
|
136
|
+
sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg &&
|
|
137
|
+
echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" | sudo tee /etc/apt/sources.list.d/brave-browser-release.list &&
|
|
138
|
+
sudo apt update &&
|
|
139
|
+
sudo apt install -y brave-browser
|
|
140
|
+
`;
|
|
141
|
+
await execAsync(cmd);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
else if (dist === 'fedora') {
|
|
145
|
+
console.error('2️⃣ Installing for Fedora/CentOS (Attempting sudo)...');
|
|
146
|
+
const cmd = `
|
|
147
|
+
sudo dnf install -y dnf-plugins-core &&
|
|
148
|
+
sudo dnf config-manager --add-repo https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo &&
|
|
149
|
+
sudo rpm --import https://brave-browser-rpm-release.s3.brave.com/brave-core.asc &&
|
|
150
|
+
sudo dnf install -y brave-browser
|
|
151
|
+
`;
|
|
152
|
+
await execAsync(cmd);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.error(' Unsupported Linux distribution for auto-install.');
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error(` Linux install failed: ${error.message}`);
|
|
162
|
+
console.error(' Note: This requires sudo/root access without password or interactive terminal.');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Download a file from URL to destination
|
|
168
|
+
*/
|
|
169
|
+
static async downloadFile(url, dest) {
|
|
170
|
+
const writer = fs.createWriteStream(dest);
|
|
171
|
+
const response = await axios({
|
|
172
|
+
url,
|
|
173
|
+
method: 'GET',
|
|
174
|
+
responseType: 'stream'
|
|
175
|
+
});
|
|
176
|
+
response.data.pipe(writer);
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
writer.on('finish', resolve);
|
|
179
|
+
writer.on('error', reject);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/browser-manager.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import * as net from 'net';
|
|
5
5
|
import { execSync, spawn } from 'child_process';
|
|
6
6
|
import { config as dotenvConfig } from 'dotenv';
|
|
7
|
+
import { BraveInstaller } from './brave-installer.js';
|
|
7
8
|
// Load environment variables from .env file
|
|
8
9
|
// Silence dotenv output
|
|
9
10
|
const originalWrite = process.stdout.write;
|
|
@@ -25,6 +26,12 @@ export var BrowserErrorType;
|
|
|
25
26
|
// Store browser instance
|
|
26
27
|
let browserInstance = null;
|
|
27
28
|
let pageInstance = null;
|
|
29
|
+
export function setBrowser(browser) {
|
|
30
|
+
browserInstance = browser;
|
|
31
|
+
}
|
|
32
|
+
export function setPage(page) {
|
|
33
|
+
pageInstance = page;
|
|
34
|
+
}
|
|
28
35
|
// Check environment variable for testing override
|
|
29
36
|
const disableContentPriority = process.env.DISABLE_CONTENT_PRIORITY === 'true' || process.env.NODE_ENV === 'test';
|
|
30
37
|
let contentPriorityConfig = {
|
|
@@ -431,7 +438,21 @@ export async function initializeBrowser(options) {
|
|
|
431
438
|
await closeBrowser();
|
|
432
439
|
}
|
|
433
440
|
}
|
|
434
|
-
|
|
441
|
+
let detectedBravePath = detectBravePath();
|
|
442
|
+
const autoInstall = options?.autoInstall ?? true;
|
|
443
|
+
if (!detectedBravePath && autoInstall) {
|
|
444
|
+
console.error('⚠️ Brave Browser not found. autoInstall is enabled.');
|
|
445
|
+
const installed = await BraveInstaller.install();
|
|
446
|
+
if (installed) {
|
|
447
|
+
console.error('✅ Installation triggered. Retrying detection...');
|
|
448
|
+
// Wait a bit to ensure it's registered
|
|
449
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
450
|
+
detectedBravePath = detectBravePath();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!detectedBravePath && !options?.customConfig?.chromePath) {
|
|
454
|
+
throw new Error('Brave Browser not found and auto-install failed or disabled. Please install Brave Browser manually: https://brave.com/download/');
|
|
455
|
+
}
|
|
435
456
|
const customConfig = options?.customConfig ?? {};
|
|
436
457
|
const platform = process.platform;
|
|
437
458
|
const getOptimalBraveFlags = (isWindows, isRetry = false) => {
|
|
@@ -25,11 +25,11 @@ export async function handleExtractJSON(args) {
|
|
|
25
25
|
const defaultSelector = selector || 'script[type="application/json"], script[type="application/ld+json"], script';
|
|
26
26
|
const scripts = document.querySelectorAll(defaultSelector);
|
|
27
27
|
scripts.forEach((script, index) => {
|
|
28
|
+
const content = script.textContent || '';
|
|
28
29
|
try {
|
|
29
|
-
|
|
30
|
+
// 1. Try direct parsing first
|
|
30
31
|
const data = JSON.parse(content);
|
|
31
32
|
if (filter) {
|
|
32
|
-
// Simple filter check
|
|
33
33
|
const filterLower = filter.toLowerCase();
|
|
34
34
|
const dataStr = JSON.stringify(data).toLowerCase();
|
|
35
35
|
if (!dataStr.includes(filterLower))
|
|
@@ -42,7 +42,37 @@ export async function handleExtractJSON(args) {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
catch (e) {
|
|
45
|
-
//
|
|
45
|
+
// 2. Fallback: Try to find JSON objects using regex
|
|
46
|
+
// Matches { "key": ... } or [ ... ] structures
|
|
47
|
+
const jsonRegex = /({[\s\S]*?}|\[[\s\S]*?\])/g;
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = jsonRegex.exec(content)) !== null) {
|
|
50
|
+
const potentialJson = match[0];
|
|
51
|
+
// Basic heuristic to avoid trying to parse tiny fragments
|
|
52
|
+
if (potentialJson.length < 20)
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const data = JSON.parse(potentialJson);
|
|
56
|
+
// Check filter
|
|
57
|
+
if (filter) {
|
|
58
|
+
const filterLower = filter.toLowerCase();
|
|
59
|
+
const dataStr = JSON.stringify(data).toLowerCase();
|
|
60
|
+
if (!dataStr.includes(filterLower))
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Basic check to ensure it's a nontrivial object/array
|
|
64
|
+
if ((Array.isArray(data) && data.length > 0) || (typeof data === 'object' && data !== null && Object.keys(data).length > 0)) {
|
|
65
|
+
results.push({
|
|
66
|
+
data,
|
|
67
|
+
source: 'script',
|
|
68
|
+
path: `script[${index}]_regex_match`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e2) {
|
|
73
|
+
// Not valid JSON
|
|
74
|
+
}
|
|
75
|
+
}
|
|
46
76
|
}
|
|
47
77
|
});
|
|
48
78
|
}
|
|
@@ -67,73 +67,6 @@ export async function handleBatchElementScraper(args) {
|
|
|
67
67
|
};
|
|
68
68
|
}, 'Failed to batch scrape elements');
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Parent-child relationships maintain करते हुए data निकालता है
|
|
72
|
-
*/
|
|
73
|
-
export async function handleNestedDataExtraction(args) {
|
|
74
|
-
return await withErrorHandling(async () => {
|
|
75
|
-
validateWorkflow('nested_data_extraction', {
|
|
76
|
-
requireBrowser: true,
|
|
77
|
-
requirePage: true,
|
|
78
|
-
});
|
|
79
|
-
const page = getCurrentPage();
|
|
80
|
-
const parentSelector = args.parentSelector;
|
|
81
|
-
const childSelector = args.childSelector;
|
|
82
|
-
const maxParents = args.maxParents || 50;
|
|
83
|
-
const nestedData = await page.evaluate(({ parentSelector, childSelector, maxParents }) => {
|
|
84
|
-
const parents = document.querySelectorAll(parentSelector);
|
|
85
|
-
const results = [];
|
|
86
|
-
let count = 0;
|
|
87
|
-
parents.forEach((parent) => {
|
|
88
|
-
if (count >= maxParents)
|
|
89
|
-
return;
|
|
90
|
-
const parentData = {
|
|
91
|
-
selector: parentSelector,
|
|
92
|
-
text: Array.from(parent.childNodes)
|
|
93
|
-
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
|
94
|
-
.map((node) => node.textContent?.trim())
|
|
95
|
-
.filter((text) => text)
|
|
96
|
-
.join(' '),
|
|
97
|
-
attributes: {},
|
|
98
|
-
};
|
|
99
|
-
// Get parent attributes
|
|
100
|
-
Array.from(parent.attributes).forEach((attr) => {
|
|
101
|
-
parentData.attributes[attr.name] = attr.value;
|
|
102
|
-
});
|
|
103
|
-
// Get children
|
|
104
|
-
const children = parent.querySelectorAll(childSelector);
|
|
105
|
-
const childrenData = [];
|
|
106
|
-
children.forEach((child) => {
|
|
107
|
-
const childData = {
|
|
108
|
-
selector: childSelector,
|
|
109
|
-
text: child.textContent?.trim() || '',
|
|
110
|
-
attributes: {},
|
|
111
|
-
};
|
|
112
|
-
Array.from(child.attributes).forEach((attr) => {
|
|
113
|
-
childData.attributes[attr.name] = attr.value;
|
|
114
|
-
});
|
|
115
|
-
childrenData.push(childData);
|
|
116
|
-
});
|
|
117
|
-
if (childrenData.length > 0) {
|
|
118
|
-
results.push({
|
|
119
|
-
parent: parentData,
|
|
120
|
-
children: childrenData,
|
|
121
|
-
});
|
|
122
|
-
count++;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
return results;
|
|
126
|
-
}, { parentSelector, childSelector, maxParents });
|
|
127
|
-
return {
|
|
128
|
-
content: [
|
|
129
|
-
{
|
|
130
|
-
type: 'text',
|
|
131
|
-
text: `✅ Extracted ${nestedData.length} parent-child relationships\n\n${JSON.stringify(nestedData, null, 2)}`,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
};
|
|
135
|
-
}, 'Failed to extract nested data');
|
|
136
|
-
}
|
|
137
70
|
/**
|
|
138
71
|
* सभी elements के attributes (href, src, data-*) collect करता है
|
|
139
72
|
*/
|
|
@@ -213,6 +146,10 @@ export async function handleLinkHarvester(args) {
|
|
|
213
146
|
const results = [];
|
|
214
147
|
links.forEach((link, index) => {
|
|
215
148
|
const href = link.href;
|
|
149
|
+
// Skip if no href (e.g. button without href)
|
|
150
|
+
if (!href) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
216
153
|
// Skip anchors if not included
|
|
217
154
|
if (!includeAnchors && href.startsWith('#')) {
|
|
218
155
|
return;
|
|
@@ -244,6 +181,7 @@ export async function handleLinkHarvester(args) {
|
|
|
244
181
|
}
|
|
245
182
|
catch (e) {
|
|
246
183
|
linkInfo.type = 'invalid';
|
|
184
|
+
linkInfo.domain = 'unknown'; // Ensure domain exists even if invalid
|
|
247
185
|
}
|
|
248
186
|
}
|
|
249
187
|
// Additional attributes
|
|
@@ -143,3 +143,62 @@ async function withWorkflowValidation(toolName, args, operation) {
|
|
|
143
143
|
throw error;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Site structure follow करके pages scrape करता है
|
|
148
|
+
*/
|
|
149
|
+
export async function handleBreadcrumbNavigator(args) {
|
|
150
|
+
return await withWorkflowValidation('breadcrumb_navigator', args, async () => {
|
|
151
|
+
return await withErrorHandling(async () => {
|
|
152
|
+
const page = getPageInstance();
|
|
153
|
+
if (!page) {
|
|
154
|
+
throw new Error('Browser not initialized. Call browser_init first.');
|
|
155
|
+
}
|
|
156
|
+
const breadcrumbSelector = args.breadcrumbSelector || '.breadcrumb, nav[aria-label="breadcrumb"], .breadcrumbs';
|
|
157
|
+
const followLinks = args.followLinks || false;
|
|
158
|
+
const breadcrumbData = await page.evaluate((selector) => {
|
|
159
|
+
const breadcrumbs = document.querySelectorAll(selector);
|
|
160
|
+
const results = [];
|
|
161
|
+
breadcrumbs.forEach((breadcrumb) => {
|
|
162
|
+
const links = breadcrumb.querySelectorAll('a');
|
|
163
|
+
const items = [];
|
|
164
|
+
links.forEach((link, index) => {
|
|
165
|
+
items.push({
|
|
166
|
+
text: link.textContent?.trim() || '',
|
|
167
|
+
href: link.href,
|
|
168
|
+
level: index,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
if (items.length > 0) {
|
|
172
|
+
results.push({
|
|
173
|
+
path: items.map((i) => i.text).join(' > '),
|
|
174
|
+
links: items,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return results;
|
|
179
|
+
}, breadcrumbSelector);
|
|
180
|
+
if (breadcrumbData.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: 'text',
|
|
185
|
+
text: '❌ No breadcrumbs found on page',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
let additionalData = '';
|
|
191
|
+
if (followLinks && breadcrumbData[0]?.links) {
|
|
192
|
+
additionalData = `\n\n📌 To scrape breadcrumb pages, use multi_page_scraper with URLs: ${JSON.stringify(breadcrumbData[0].links.map((l) => l.href))}`;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: `✅ Found ${breadcrumbData.length} breadcrumb trail(s)\n\n${JSON.stringify(breadcrumbData, null, 2)}${additionalData}`,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}, 'Failed to navigate breadcrumbs');
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -262,124 +262,3 @@ export async function handleAdvancedCSSSelectors(args) {
|
|
|
262
262
|
return { content: [{ type: 'text', text: `❌ CSS selector query failed: ${error.message}` }], isError: true };
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
-
/**
|
|
266
|
-
* Visual Element Finder - Find elements by visual properties
|
|
267
|
-
*/
|
|
268
|
-
export async function handleVisualElementFinder(args) {
|
|
269
|
-
const { url, criteria } = args;
|
|
270
|
-
try {
|
|
271
|
-
const page = getPageInstance();
|
|
272
|
-
if (!page) {
|
|
273
|
-
throw new Error('Browser not initialized. Call browser_init first.');
|
|
274
|
-
}
|
|
275
|
-
if (url && page.url() !== url) {
|
|
276
|
-
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
277
|
-
}
|
|
278
|
-
const results = await page.evaluate((crit) => {
|
|
279
|
-
const allElements = Array.from(document.querySelectorAll('*'));
|
|
280
|
-
const matches = [];
|
|
281
|
-
allElements.forEach(element => {
|
|
282
|
-
const computed = window.getComputedStyle(element);
|
|
283
|
-
const rect = element.getBoundingClientRect();
|
|
284
|
-
let matchScore = 0;
|
|
285
|
-
const reasons = [];
|
|
286
|
-
// Check visibility
|
|
287
|
-
if (crit.visible !== undefined) {
|
|
288
|
-
const isVisible = computed.display !== 'none' &&
|
|
289
|
-
computed.visibility !== 'hidden' &&
|
|
290
|
-
rect.width > 0 &&
|
|
291
|
-
rect.height > 0;
|
|
292
|
-
if (isVisible === crit.visible) {
|
|
293
|
-
matchScore += 10;
|
|
294
|
-
reasons.push('visibility');
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// Check color
|
|
298
|
-
if (crit.color) {
|
|
299
|
-
if (computed.color.includes(crit.color) || computed.backgroundColor.includes(crit.color)) {
|
|
300
|
-
matchScore += 5;
|
|
301
|
-
reasons.push('color');
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// Check size
|
|
305
|
-
if (crit.minWidth && rect.width >= crit.minWidth) {
|
|
306
|
-
matchScore += 3;
|
|
307
|
-
reasons.push('minWidth');
|
|
308
|
-
}
|
|
309
|
-
if (crit.maxWidth && rect.width <= crit.maxWidth) {
|
|
310
|
-
matchScore += 3;
|
|
311
|
-
reasons.push('maxWidth');
|
|
312
|
-
}
|
|
313
|
-
if (crit.minHeight && rect.height >= crit.minHeight) {
|
|
314
|
-
matchScore += 3;
|
|
315
|
-
reasons.push('minHeight');
|
|
316
|
-
}
|
|
317
|
-
if (crit.maxHeight && rect.height <= crit.maxHeight) {
|
|
318
|
-
matchScore += 3;
|
|
319
|
-
reasons.push('maxHeight');
|
|
320
|
-
}
|
|
321
|
-
// Check position
|
|
322
|
-
if (crit.position) {
|
|
323
|
-
if (computed.position === crit.position) {
|
|
324
|
-
matchScore += 5;
|
|
325
|
-
reasons.push('position');
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
// Check text content
|
|
329
|
-
if (crit.hasText !== undefined) {
|
|
330
|
-
const hasText = (element.textContent?.trim().length || 0) > 0;
|
|
331
|
-
if (hasText === crit.hasText) {
|
|
332
|
-
matchScore += 5;
|
|
333
|
-
reasons.push('hasText');
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Check if element is in viewport
|
|
337
|
-
if (crit.inViewport !== undefined) {
|
|
338
|
-
const inViewport = rect.top >= 0 &&
|
|
339
|
-
rect.left >= 0 &&
|
|
340
|
-
rect.bottom <= window.innerHeight &&
|
|
341
|
-
rect.right <= window.innerWidth;
|
|
342
|
-
if (inViewport === crit.inViewport) {
|
|
343
|
-
matchScore += 5;
|
|
344
|
-
reasons.push('inViewport');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
if (matchScore > 0) {
|
|
348
|
-
matches.push({
|
|
349
|
-
element: {
|
|
350
|
-
tagName: element.tagName.toLowerCase(),
|
|
351
|
-
id: element.id,
|
|
352
|
-
className: element.className,
|
|
353
|
-
text: element.textContent?.substring(0, 100)
|
|
354
|
-
},
|
|
355
|
-
score: matchScore,
|
|
356
|
-
matchedCriteria: reasons,
|
|
357
|
-
visualProperties: {
|
|
358
|
-
display: computed.display,
|
|
359
|
-
visibility: computed.visibility,
|
|
360
|
-
position: computed.position,
|
|
361
|
-
color: computed.color,
|
|
362
|
-
backgroundColor: computed.backgroundColor,
|
|
363
|
-
width: rect.width,
|
|
364
|
-
height: rect.height,
|
|
365
|
-
top: rect.top,
|
|
366
|
-
left: rect.left
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
matches.sort((a, b) => b.score - a.score);
|
|
372
|
-
return {
|
|
373
|
-
totalMatches: matches.length,
|
|
374
|
-
topMatches: matches.slice(0, 20)
|
|
375
|
-
};
|
|
376
|
-
}, criteria);
|
|
377
|
-
const resultText = `✅ Visual Element Finder Results\n\nCriteria: ${JSON.stringify(criteria, null, 2)}\nTotal Matches: ${results.totalMatches}\n\nTop Matches:\n${JSON.stringify(results.topMatches, null, 2)}`;
|
|
378
|
-
return {
|
|
379
|
-
content: [{ type: 'text', text: resultText }],
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
return { content: [{ type: 'text', text: `❌ Visual element finder failed: ${error.message}` }], isError: true };
|
|
384
|
-
}
|
|
385
|
-
}
|
|
@@ -307,7 +307,18 @@ export async function handleFetchXHR(args) {
|
|
|
307
307
|
});
|
|
308
308
|
const page = getCurrentPage();
|
|
309
309
|
const duration = args.duration || 15000;
|
|
310
|
+
const forceReload = args.forceReload !== false; // Default true to capture initial requests
|
|
310
311
|
const xhrData = [];
|
|
312
|
+
// Capture requests too for completeness
|
|
313
|
+
const requestHandler = (request) => {
|
|
314
|
+
try {
|
|
315
|
+
const resourceType = request.resourceType();
|
|
316
|
+
if (resourceType === 'xhr' || resourceType === 'fetch') {
|
|
317
|
+
// Optional: Log request if needed, but for now we focus on responses with bodies
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (e) { }
|
|
321
|
+
};
|
|
311
322
|
const responseHandler = async (response) => {
|
|
312
323
|
const request = response.request();
|
|
313
324
|
const resourceType = request.resourceType();
|
|
@@ -320,7 +331,8 @@ export async function handleFetchXHR(args) {
|
|
|
320
331
|
statusText: response.statusText(),
|
|
321
332
|
headers: response.headers(),
|
|
322
333
|
method: request.method(),
|
|
323
|
-
|
|
334
|
+
postData: request.postData(),
|
|
335
|
+
body: body.substring(0, 5000), // Increased limit
|
|
324
336
|
timestamp: new Date().toISOString(),
|
|
325
337
|
});
|
|
326
338
|
}
|
|
@@ -330,6 +342,14 @@ export async function handleFetchXHR(args) {
|
|
|
330
342
|
}
|
|
331
343
|
};
|
|
332
344
|
page.on('response', responseHandler);
|
|
345
|
+
if (forceReload) {
|
|
346
|
+
try {
|
|
347
|
+
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
// Continue even if reload times out
|
|
351
|
+
}
|
|
352
|
+
}
|
|
333
353
|
await sleep(duration);
|
|
334
354
|
page.off('response', responseHandler);
|
|
335
355
|
return {
|