brave-real-browser-mcp-server 2.41.0 → 2.41.2

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.
Binary file
@@ -0,0 +1,525 @@
1
+ /**
2
+ * OCR Text Captcha Solver (Enhanced Version)
3
+ *
4
+ * Simple text-based captcha solver using Tesseract.js OCR
5
+ * Works with captchas like: CCA23E, actukd, hf4kvf (eCourts India style)
6
+ *
7
+ * Features:
8
+ * - Advanced image preprocessing (grayscale, threshold, denoise, remove lines)
9
+ * - Multiple OCR attempts with different settings
10
+ * - Auto-retry with captcha refresh
11
+ * - Hindi + English language support
12
+ * - Works offline (no API needed)
13
+ */
14
+
15
+ const Tesseract = require('tesseract.js');
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+
19
+ // Colors for console
20
+ const colors = {
21
+ green: '\x1b[32m',
22
+ yellow: '\x1b[33m',
23
+ blue: '\x1b[34m',
24
+ red: '\x1b[31m',
25
+ cyan: '\x1b[36m',
26
+ reset: '\x1b[0m'
27
+ };
28
+
29
+ const log = {
30
+ info: (msg) => console.log(`${colors.blue}[ocr-captcha]${colors.reset} ${msg}`),
31
+ success: (msg) => console.log(`${colors.green}[ocr-captcha]${colors.reset} ✅ ${msg}`),
32
+ warn: (msg) => console.log(`${colors.yellow}[ocr-captcha]${colors.reset} ⚠️ ${msg}`),
33
+ error: (msg) => console.log(`${colors.red}[ocr-captcha]${colors.reset} ❌ ${msg}`),
34
+ debug: (msg) => console.log(`${colors.cyan}[ocr-captcha]${colors.reset} 🔍 ${msg}`)
35
+ };
36
+
37
+ // Common captcha character substitutions for correction
38
+ const CHAR_SUBSTITUTIONS = {
39
+ '0': ['O', 'o', 'Q', 'D'],
40
+ 'O': ['0', 'o', 'Q', 'D'],
41
+ '1': ['l', 'I', 'i', '|', '!'],
42
+ 'l': ['1', 'I', 'i', '|'],
43
+ 'I': ['1', 'l', 'i', '|'],
44
+ '5': ['S', 's', '$'],
45
+ 'S': ['5', 's', '$'],
46
+ '8': ['B', '&'],
47
+ 'B': ['8', '&'],
48
+ '2': ['Z', 'z'],
49
+ 'Z': ['2', 'z'],
50
+ '6': ['G', 'b'],
51
+ 'G': ['6', 'C'],
52
+ '9': ['g', 'q'],
53
+ 'g': ['9', 'q'],
54
+ 'C': ['G', 'c', '('],
55
+ 'E': ['3', 'e'],
56
+ '3': ['E', 'e'],
57
+ 'A': ['4', 'a'],
58
+ '4': ['A', 'a'],
59
+ };
60
+
61
+ // Default OCR settings optimized for captchas
62
+ const DEFAULT_OCR_CONFIG = {
63
+ lang: 'eng',
64
+ tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
65
+ tessedit_pageseg_mode: '7', // Single line
66
+ preserve_interword_spaces: '0',
67
+ };
68
+
69
+ // Preprocessing configurations for different captcha types
70
+ const PREPROCESS_CONFIGS = [
71
+ { name: 'standard', threshold: 128, invert: false, removeLines: true },
72
+ { name: 'high-contrast', threshold: 100, invert: false, removeLines: true },
73
+ { name: 'low-contrast', threshold: 160, invert: false, removeLines: true },
74
+ { name: 'inverted', threshold: 128, invert: true, removeLines: true },
75
+ { name: 'no-line-removal', threshold: 128, invert: false, removeLines: false },
76
+ ];
77
+
78
+ /**
79
+ * Advanced image preprocessing in browser
80
+ * Removes noise, lines, and enhances text
81
+ */
82
+ async function preprocessImageAdvanced(page, selector, config = {}) {
83
+ const { threshold = 128, invert = false, removeLines = true } = config;
84
+
85
+ return await page.evaluate(({ sel, threshold, invert, removeLines }) => {
86
+ const img = document.querySelector(sel);
87
+ if (!img) return null;
88
+
89
+ const canvas = document.createElement('canvas');
90
+ const ctx = canvas.getContext('2d');
91
+
92
+ // Use natural dimensions for better quality
93
+ const width = img.naturalWidth || img.width || 200;
94
+ const height = img.naturalHeight || img.height || 50;
95
+
96
+ canvas.width = width;
97
+ canvas.height = height;
98
+
99
+ // Draw original image
100
+ ctx.drawImage(img, 0, 0, width, height);
101
+
102
+ // Get image data
103
+ const imageData = ctx.getImageData(0, 0, width, height);
104
+ const data = imageData.data;
105
+
106
+ // Step 1: Convert to grayscale
107
+ for (let i = 0; i < data.length; i += 4) {
108
+ const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
109
+ data[i] = gray;
110
+ data[i + 1] = gray;
111
+ data[i + 2] = gray;
112
+ }
113
+
114
+ // Step 2: Remove diagonal lines (common in captchas)
115
+ if (removeLines) {
116
+ // Detect and remove thin lines
117
+ for (let y = 1; y < height - 1; y++) {
118
+ for (let x = 1; x < width - 1; x++) {
119
+ const idx = (y * width + x) * 4;
120
+ const pixel = data[idx];
121
+
122
+ // Check if this is a dark pixel
123
+ if (pixel < threshold) {
124
+ // Get surrounding pixels
125
+ const top = data[((y - 1) * width + x) * 4];
126
+ const bottom = data[((y + 1) * width + x) * 4];
127
+ const left = data[(y * width + (x - 1)) * 4];
128
+ const right = data[(y * width + (x + 1)) * 4];
129
+
130
+ // Count dark neighbors
131
+ let darkNeighbors = 0;
132
+ if (top < threshold) darkNeighbors++;
133
+ if (bottom < threshold) darkNeighbors++;
134
+ if (left < threshold) darkNeighbors++;
135
+ if (right < threshold) darkNeighbors++;
136
+
137
+ // If isolated or thin line (≤2 dark neighbors), might be noise
138
+ if (darkNeighbors <= 1) {
139
+ // Make it white (remove noise)
140
+ data[idx] = 255;
141
+ data[idx + 1] = 255;
142
+ data[idx + 2] = 255;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ // Step 3: Apply threshold (binarization)
150
+ for (let i = 0; i < data.length; i += 4) {
151
+ const gray = data[i];
152
+ let bw = gray > threshold ? 255 : 0;
153
+
154
+ // Invert if needed
155
+ if (invert) bw = 255 - bw;
156
+
157
+ data[i] = bw;
158
+ data[i + 1] = bw;
159
+ data[i + 2] = bw;
160
+ }
161
+
162
+ ctx.putImageData(imageData, 0, 0);
163
+
164
+ return canvas.toDataURL('image/png');
165
+ }, { sel: selector, threshold, invert, removeLines });
166
+ }
167
+
168
+ /**
169
+ * Get captcha image with optional preprocessing
170
+ */
171
+ async function getCaptchaImage(page, selector, preprocess = true, preprocessConfig = {}) {
172
+ try {
173
+ if (preprocess) {
174
+ // Try preprocessing first
175
+ const processed = await preprocessImageAdvanced(page, selector, preprocessConfig);
176
+ if (processed) {
177
+ log.debug('Image preprocessed successfully');
178
+ return processed;
179
+ }
180
+ }
181
+
182
+ // Fallback: Screenshot of element
183
+ const element = await page.$(selector);
184
+ if (element) {
185
+ const screenshot = await element.screenshot({ encoding: 'base64' });
186
+ return `data:image/png;base64,${screenshot}`;
187
+ }
188
+
189
+ // Fallback 2: Get image src
190
+ const imgSrc = await page.$eval(selector, (img) => {
191
+ if (img.tagName === 'IMG') {
192
+ const canvas = document.createElement('canvas');
193
+ canvas.width = img.naturalWidth || img.width;
194
+ canvas.height = img.naturalHeight || img.height;
195
+ const ctx = canvas.getContext('2d');
196
+ ctx.drawImage(img, 0, 0);
197
+ return canvas.toDataURL('image/png');
198
+ }
199
+ return img.src || null;
200
+ });
201
+
202
+ return imgSrc;
203
+ } catch (error) {
204
+ log.warn(`Image capture error: ${error.message}`);
205
+ return null;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Recognize text using Tesseract with multiple attempts
211
+ */
212
+ async function recognizeText(imageData, config = {}) {
213
+ const worker = await Tesseract.createWorker(config.lang || 'eng');
214
+
215
+ try {
216
+ await worker.setParameters({
217
+ tessedit_char_whitelist: config.tessedit_char_whitelist || DEFAULT_OCR_CONFIG.tessedit_char_whitelist,
218
+ tessedit_pageseg_mode: config.tessedit_pageseg_mode || '7',
219
+ });
220
+
221
+ const { data } = await worker.recognize(imageData);
222
+
223
+ // Clean up recognized text
224
+ let text = data.text
225
+ .replace(/\s+/g, '') // Remove whitespace
226
+ .replace(/[^a-zA-Z0-9]/g, ''); // Keep only alphanumeric
227
+
228
+ return {
229
+ text,
230
+ confidence: data.confidence,
231
+ rawText: data.text,
232
+ };
233
+ } finally {
234
+ await worker.terminate();
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Solve text captcha with multiple preprocessing attempts
240
+ */
241
+ async function solveTextCaptcha(page, selector, options = {}) {
242
+ const {
243
+ lang = 'eng',
244
+ retries = 3,
245
+ confidence = 50, // Lowered for better success rate
246
+ allowedChars = null,
247
+ expectedLength = null,
248
+ tryAllPreprocess = true, // Try all preprocessing configs
249
+ } = options;
250
+
251
+ log.info(`Solving text captcha: ${selector}`);
252
+
253
+ try {
254
+ let bestResult = null;
255
+ let allAttempts = [];
256
+
257
+ // Determine which preprocessing configs to try
258
+ const configsToTry = tryAllPreprocess ? PREPROCESS_CONFIGS : [PREPROCESS_CONFIGS[0]];
259
+
260
+ for (const preprocessConfig of configsToTry) {
261
+ log.debug(`Trying preprocess config: ${preprocessConfig.name}`);
262
+
263
+ // Get preprocessed image
264
+ const imageData = await getCaptchaImage(page, selector, true, preprocessConfig);
265
+
266
+ if (!imageData) {
267
+ continue;
268
+ }
269
+
270
+ // Try different PSM modes
271
+ const psmModes = ['7', '8', '13', '6']; // 7=single line, 8=word, 13=raw, 6=block
272
+
273
+ for (let i = 0; i < Math.min(retries, psmModes.length); i++) {
274
+ const config = {
275
+ ...DEFAULT_OCR_CONFIG,
276
+ lang,
277
+ tessedit_pageseg_mode: psmModes[i],
278
+ ...(allowedChars && { tessedit_char_whitelist: allowedChars }),
279
+ };
280
+
281
+ try {
282
+ const result = await recognizeText(imageData, config);
283
+ result.preprocessConfig = preprocessConfig.name;
284
+ result.psmMode = psmModes[i];
285
+ allAttempts.push(result);
286
+
287
+ log.debug(` PSM ${psmModes[i]}: "${result.text}" (${result.confidence.toFixed(1)}%)`);
288
+
289
+ // Check if result meets criteria
290
+ const meetsConfidence = result.confidence >= confidence;
291
+ const meetsLength = !expectedLength || result.text.length === expectedLength;
292
+ const hasText = result.text.length > 0;
293
+
294
+ if (meetsConfidence && meetsLength && hasText) {
295
+ bestResult = result;
296
+ break;
297
+ }
298
+
299
+ // Keep best result so far (prioritize correct length)
300
+ if (hasText) {
301
+ if (!bestResult) {
302
+ bestResult = result;
303
+ } else if (expectedLength) {
304
+ // Prefer correct length
305
+ if (result.text.length === expectedLength && bestResult.text.length !== expectedLength) {
306
+ bestResult = result;
307
+ } else if (result.confidence > bestResult.confidence) {
308
+ bestResult = result;
309
+ }
310
+ } else if (result.confidence > bestResult.confidence) {
311
+ bestResult = result;
312
+ }
313
+ }
314
+ } catch (err) {
315
+ log.debug(` OCR error: ${err.message}`);
316
+ }
317
+ }
318
+
319
+ // Stop if we found a good result
320
+ if (bestResult && bestResult.confidence >= confidence) {
321
+ break;
322
+ }
323
+ }
324
+
325
+ if (bestResult && bestResult.text) {
326
+ log.success(`Solved: "${bestResult.text}" (confidence: ${bestResult.confidence.toFixed(1)}%, config: ${bestResult.preprocessConfig})`);
327
+ return {
328
+ success: true,
329
+ text: bestResult.text,
330
+ confidence: bestResult.confidence,
331
+ attempts: allAttempts.length,
332
+ allAttempts,
333
+ };
334
+ }
335
+
336
+ log.warn('OCR could not recognize text');
337
+ return {
338
+ success: false,
339
+ text: bestResult?.text || '',
340
+ confidence: bestResult?.confidence || 0,
341
+ attempts: allAttempts.length,
342
+ allAttempts,
343
+ };
344
+
345
+ } catch (error) {
346
+ log.error(`Failed to solve captcha: ${error.message}`);
347
+ return {
348
+ success: false,
349
+ text: '',
350
+ confidence: 0,
351
+ error: error.message,
352
+ };
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Solve captcha and fill input field with auto-retry
358
+ */
359
+ async function solveCaptchaAndFill(page, captchaSelector, inputSelector, options = {}) {
360
+ const {
361
+ humanLike = true,
362
+ submitAfter = false,
363
+ submitSelector = 'button[type="submit"], input[type="submit"], button.btn-primary, input.btn',
364
+ refreshSelector = null,
365
+ maxRefreshAttempts = 5, // Increased retries
366
+ minConfidence = 40, // Minimum confidence to try
367
+ expectedLength = null,
368
+ lang = 'eng',
369
+ allowedChars = null,
370
+ waitAfterRefresh = 1500, // Wait after refresh
371
+ waitBeforeType = 500, // Wait before typing
372
+ } = options;
373
+
374
+ let attempts = 0;
375
+ let lastResult = null;
376
+
377
+ log.info(`Starting captcha solve with ${maxRefreshAttempts} max attempts`);
378
+
379
+ while (attempts < maxRefreshAttempts) {
380
+ attempts++;
381
+ log.info(`Attempt ${attempts}/${maxRefreshAttempts}`);
382
+
383
+ // Wait for image to load properly
384
+ await new Promise(r => setTimeout(r, waitBeforeType));
385
+
386
+ // Solve captcha
387
+ const result = await solveTextCaptcha(page, captchaSelector, {
388
+ lang,
389
+ expectedLength,
390
+ allowedChars,
391
+ confidence: minConfidence,
392
+ tryAllPreprocess: attempts <= 2, // Only try all configs on first 2 attempts
393
+ });
394
+
395
+ lastResult = result;
396
+
397
+ // Check if we got a usable result
398
+ if (!result.text || result.text.length === 0) {
399
+ log.warn('No text recognized');
400
+ if (refreshSelector) {
401
+ log.info('Refreshing captcha...');
402
+ try {
403
+ await page.click(refreshSelector);
404
+ await new Promise(r => setTimeout(r, waitAfterRefresh));
405
+ } catch (e) {
406
+ log.warn(`Refresh failed: ${e.message}`);
407
+ }
408
+ }
409
+ continue;
410
+ }
411
+
412
+ // If expected length specified and doesn't match, refresh
413
+ if (expectedLength && result.text.length !== expectedLength) {
414
+ log.warn(`Length mismatch: got ${result.text.length}, expected ${expectedLength}`);
415
+ if (refreshSelector && attempts < maxRefreshAttempts) {
416
+ log.info('Refreshing captcha for better result...');
417
+ await page.click(refreshSelector);
418
+ await new Promise(r => setTimeout(r, waitAfterRefresh));
419
+ continue;
420
+ }
421
+ }
422
+
423
+ // Fill the input
424
+ const input = await page.$(inputSelector);
425
+ if (!input) {
426
+ return { success: false, error: 'Input field not found', attempts };
427
+ }
428
+
429
+ // Clear field
430
+ await page.click(inputSelector, { clickCount: 3 });
431
+ await page.keyboard.press('Backspace');
432
+ await new Promise(r => setTimeout(r, 100));
433
+
434
+ // Type the captcha
435
+ if (humanLike) {
436
+ for (const char of result.text) {
437
+ await page.keyboard.type(char);
438
+ await new Promise(r => setTimeout(r, 30 + Math.random() * 80));
439
+ }
440
+ } else {
441
+ await page.type(inputSelector, result.text);
442
+ }
443
+
444
+ log.success(`Filled captcha: "${result.text}"`);
445
+
446
+ // Submit if requested
447
+ if (submitAfter) {
448
+ try {
449
+ await page.click(submitSelector);
450
+ await new Promise(r => setTimeout(r, 2000));
451
+
452
+ // Check if captcha was wrong
453
+ const stillOnPage = await page.$(captchaSelector);
454
+ const errorVisible = await page.evaluate(() => {
455
+ const errorSelectors = ['.error', '.alert-danger', '.captcha-error', '#captchaError'];
456
+ return errorSelectors.some(sel => {
457
+ const el = document.querySelector(sel);
458
+ return el && el.offsetParent !== null;
459
+ });
460
+ });
461
+
462
+ if ((stillOnPage || errorVisible) && refreshSelector) {
463
+ log.warn('Captcha might be wrong, retrying...');
464
+ await page.click(refreshSelector);
465
+ await new Promise(r => setTimeout(r, waitAfterRefresh));
466
+ continue;
467
+ }
468
+ } catch (e) {
469
+ log.warn(`Submit error: ${e.message}`);
470
+ }
471
+ }
472
+
473
+ return {
474
+ success: true,
475
+ text: result.text,
476
+ confidence: result.confidence,
477
+ attempts,
478
+ };
479
+ }
480
+
481
+ return {
482
+ success: false,
483
+ error: 'Max attempts reached',
484
+ attempts,
485
+ lastResult,
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Solve captcha from URL directly
491
+ */
492
+ async function solveCaptchaFromUrl(imageUrl, options = {}) {
493
+ log.info(`Solving captcha from URL: ${imageUrl}`);
494
+
495
+ try {
496
+ const result = await recognizeText(imageUrl, {
497
+ ...DEFAULT_OCR_CONFIG,
498
+ ...options,
499
+ });
500
+
501
+ return {
502
+ success: result.confidence > 40 && result.text.length > 0,
503
+ text: result.text,
504
+ confidence: result.confidence,
505
+ };
506
+ } catch (error) {
507
+ return {
508
+ success: false,
509
+ error: error.message,
510
+ };
511
+ }
512
+ }
513
+
514
+ // Export functions
515
+ module.exports = {
516
+ solveTextCaptcha,
517
+ solveCaptchaAndFill,
518
+ solveCaptchaFromUrl,
519
+ getCaptchaImage,
520
+ recognizeText,
521
+ preprocessImageAdvanced,
522
+ CHAR_SUBSTITUTIONS,
523
+ DEFAULT_OCR_CONFIG,
524
+ PREPROCESS_CONFIGS,
525
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.41.0",
3
+ "version": "2.41.2",
4
4
  "description": "MCP Server for Brave Real Browser - Puppeteer with Brave Browser, Stealth Mode, Ad Blocker, and Turnstile Auto-Solver for undetectable web automation.",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.mjs",
@@ -75,10 +75,11 @@
75
75
  "license": "ISC",
76
76
  "dependencies": {
77
77
  "@modelcontextprotocol/sdk": "^1.25.3",
78
- "brave-real-puppeteer-core": "^24.37.0.1",
78
+ "brave-real-puppeteer-core": "^24.36.1-brave.2",
79
79
  "ghost-cursor": "^1.4.2",
80
80
  "puppeteer-extra": "^3.3.6",
81
81
  "puppeteer-extra-plugin-stealth": "^2.11.2",
82
+ "tesseract.js": "^5.1.1",
82
83
  "tree-kill": "^1.2.2",
83
84
  "vscode-languageserver": "^9.0.1",
84
85
  "vscode-languageserver-textdocument": "^1.0.12",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-blocker",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "Advanced uBlock Origin management and stealth features for Brave Real Browser",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -64,7 +64,7 @@
64
64
  "@types/adm-zip": "^0.5.5",
65
65
  "@types/fs-extra": "^11.0.4",
66
66
  "@types/node": "^20.0.0",
67
- "brave-real-puppeteer-core": "^24.37.0.1",
67
+ "brave-real-puppeteer-core": "^24.36.1-brave.2",
68
68
  "mocha": "^10.4.0",
69
69
  "puppeteer-core": ">=24.0.0",
70
70
  "sinon": "^17.0.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-launcher",
3
- "version": "1.23.0",
3
+ "version": "1.23.2",
4
4
  "description": "Launch Brave Browser with ease from node. Based on chrome-launcher with Brave-specific support.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -54,7 +54,7 @@
54
54
  "typescript": "^5.0.0"
55
55
  },
56
56
  "dependencies": {
57
- "brave-real-blocker": "^1.17.0",
57
+ "brave-real-blocker": "^1.17.2",
58
58
  "escape-string-regexp": "^5.0.0",
59
59
  "is-wsl": "^3.1.0",
60
60
  "which": "^6.0.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-playwright-core",
3
- "version": "1.59.0.1",
3
+ "version": "1.59.2",
4
4
  "description": "Brave-optimized Playwright Core (v1.57.0) with comprehensive stealth patches and error stack sanitization",
5
5
  "keywords": [
6
6
  "playwright",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-puppeteer-core",
3
- "version": "24.37.0.1",
3
+ "version": "24.36.1-brave.2",
4
4
  "description": "🦁 Brave Real-World Optimized Puppeteer & Playwright Core with 1-5ms ultra-fast timing, 50+ professional stealth features, intelligent browser auto-detection, and 100% bot detection bypass. Features cross-platform Brave browser integration, comprehensive anti-detection, and breakthrough performance improvements.",
5
5
  "keywords": [
6
6
  "automation",
@@ -134,7 +134,7 @@
134
134
  "test-version": "node ./scripts/test-version-management.js"
135
135
  },
136
136
  "dependencies": {
137
- "brave-real-launcher": "^1.23.0",
137
+ "brave-real-launcher": "^1.23.2",
138
138
  "get-east-asian-width": "^1.4.0",
139
139
  "yargs": "^18.0.0"
140
140
  },
@@ -31,6 +31,9 @@ const PACKAGES = [
31
31
  ];
32
32
 
33
33
  function incrementVersion(version, type) {
34
+ // Clean the version string first
35
+ version = String(version).trim();
36
+
34
37
  // Handle versions with -patch suffix (e.g., "1.57.0-patch.15")
35
38
  const patchSuffixMatch = version.match(/^(.+)-patch\.(\d+)$/);
36
39
  if (patchSuffixMatch) {
@@ -39,26 +42,49 @@ function incrementVersion(version, type) {
39
42
  return `${baseVersion}-patch.${patchNum + 1}`;
40
43
  }
41
44
 
42
- // Standard semver handling
43
- const parts = version.split('.').map(Number);
45
+ // Handle versions with -brave suffix (e.g., "24.36.1-brave.1")
46
+ const braveSuffixMatch = version.match(/^(.+)-brave\.(\d+)$/);
47
+ if (braveSuffixMatch) {
48
+ const baseVersion = braveSuffixMatch[1];
49
+ const braveNum = parseInt(braveSuffixMatch[2], 10);
50
+ return `${baseVersion}-brave.${braveNum + 1}`;
51
+ }
52
+
53
+ // Extract only the semver part (ignore any prerelease/build metadata)
54
+ const semverMatch = version.match(/^(\d+)\.(\d+)\.(\d+)/);
55
+ if (!semverMatch) {
56
+ console.warn(` ⚠️ Invalid version format: "${version}", defaulting to 1.0.0`);
57
+ return '1.0.1';
58
+ }
59
+
60
+ // Parse version parts, ensuring valid numbers
61
+ let major = parseInt(semverMatch[1], 10) || 0;
62
+ let minor = parseInt(semverMatch[2], 10) || 0;
63
+ let patch = parseInt(semverMatch[3], 10) || 0;
64
+
65
+ // Validate - must be valid numbers
66
+ if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
67
+ console.warn(` ⚠️ Version contains NaN: "${version}", defaulting to 1.0.1`);
68
+ return '1.0.1';
69
+ }
44
70
 
45
71
  switch (type) {
46
72
  case 'major':
47
- parts[0]++;
48
- parts[1] = 0;
49
- parts[2] = 0;
73
+ major++;
74
+ minor = 0;
75
+ patch = 0;
50
76
  break;
51
77
  case 'minor':
52
- parts[1]++;
53
- parts[2] = 0;
78
+ minor++;
79
+ patch = 0;
54
80
  break;
55
81
  case 'patch':
56
82
  default:
57
- parts[2]++;
83
+ patch++;
58
84
  break;
59
85
  }
60
86
 
61
- return parts.join('.');
87
+ return `${major}.${minor}.${patch}`;
62
88
  }
63
89
 
64
90
  function getNpmVersion(packageName) {
@@ -91,12 +117,48 @@ function writePackageJson(pkgPath, data) {
91
117
  function compareVersions(v1, v2) {
92
118
  // Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
93
119
  const parse = (v) => {
120
+ v = String(v).trim();
121
+
122
+ // Handle -patch suffix
94
123
  const patchMatch = v.match(/^(.+)-patch\.(\d+)$/);
95
124
  if (patchMatch) {
96
- const base = patchMatch[1].split('.').map(Number);
97
- return [...base, parseInt(patchMatch[2], 10)];
125
+ const semverMatch = patchMatch[1].match(/^(\d+)\.(\d+)\.(\d+)/);
126
+ if (semverMatch) {
127
+ return [
128
+ parseInt(semverMatch[1], 10) || 0,
129
+ parseInt(semverMatch[2], 10) || 0,
130
+ parseInt(semverMatch[3], 10) || 0,
131
+ parseInt(patchMatch[2], 10) || 0
132
+ ];
133
+ }
134
+ }
135
+
136
+ // Handle -brave suffix
137
+ const braveMatch = v.match(/^(.+)-brave\.(\d+)$/);
138
+ if (braveMatch) {
139
+ const semverMatch = braveMatch[1].match(/^(\d+)\.(\d+)\.(\d+)/);
140
+ if (semverMatch) {
141
+ return [
142
+ parseInt(semverMatch[1], 10) || 0,
143
+ parseInt(semverMatch[2], 10) || 0,
144
+ parseInt(semverMatch[3], 10) || 0,
145
+ parseInt(braveMatch[2], 10) || 0
146
+ ];
147
+ }
148
+ }
149
+
150
+ // Standard semver parsing - extract only digits
151
+ const semverMatch = v.match(/^(\d+)\.(\d+)\.(\d+)/);
152
+ if (semverMatch) {
153
+ return [
154
+ parseInt(semverMatch[1], 10) || 0,
155
+ parseInt(semverMatch[2], 10) || 0,
156
+ parseInt(semverMatch[3], 10) || 0
157
+ ];
98
158
  }
99
- return v.split('.').map(Number);
159
+
160
+ // Fallback - return zeros
161
+ return [0, 0, 0];
100
162
  };
101
163
 
102
164
  const parts1 = parse(v1);
@@ -105,8 +167,11 @@ function compareVersions(v1, v2) {
105
167
  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
106
168
  const a = parts1[i] || 0;
107
169
  const b = parts2[i] || 0;
108
- if (a > b) return 1;
109
- if (a < b) return -1;
170
+ // Extra safety - ensure numbers
171
+ const numA = isNaN(a) ? 0 : a;
172
+ const numB = isNaN(b) ? 0 : b;
173
+ if (numA > numB) return 1;
174
+ if (numA < numB) return -1;
110
175
  }
111
176
  return 0;
112
177
  }
@@ -506,13 +506,105 @@ const handlers = {
506
506
  return { success: true, message: 'Browser closed' };
507
507
  },
508
508
 
509
- // 8. Solve Captcha
509
+ // 8. Solve Captcha (Enhanced with OCR for text captchas)
510
510
  async solve_captcha(params = {}) {
511
511
  const { page } = requireBrowser();
512
- const { type = 'auto', timeout = 30000 } = params;
512
+ const {
513
+ type = 'auto',
514
+ timeout = 30000,
515
+ // OCR-specific options
516
+ captchaSelector,
517
+ inputSelector,
518
+ refreshSelector,
519
+ lang = 'eng',
520
+ expectedLength,
521
+ allowedChars,
522
+ maxRetries = 3
523
+ } = params;
513
524
 
514
525
  notifyProgress('solve_captcha', 'started', `Solving ${type} captcha...`);
515
526
 
527
+ // Handle text/image captcha with OCR
528
+ if (type === 'text' || type === 'image') {
529
+ if (!captchaSelector) {
530
+ return { success: false, error: 'captchaSelector is required for text/image captcha' };
531
+ }
532
+
533
+ try {
534
+ // Dynamic import for OCR solver
535
+ const ocrSolver = require('../../lib/ocr-captcha-solver');
536
+
537
+ if (inputSelector) {
538
+ // Solve and fill
539
+ const result = await ocrSolver.solveCaptchaAndFill(page, captchaSelector, inputSelector, {
540
+ lang,
541
+ expectedLength,
542
+ allowedChars,
543
+ refreshSelector,
544
+ maxRefreshAttempts: maxRetries,
545
+ humanLike: true
546
+ });
547
+
548
+ notifyProgress('solve_captcha', result.success ? 'completed' : 'error',
549
+ result.success ? `OCR solved: "${result.text}"` : `OCR failed: ${result.error}`);
550
+
551
+ return {
552
+ success: result.success,
553
+ type: 'ocr',
554
+ text: result.text,
555
+ confidence: result.confidence,
556
+ attempts: result.attempts,
557
+ filled: true
558
+ };
559
+ } else {
560
+ // Just solve (don't fill)
561
+ const result = await ocrSolver.solveTextCaptcha(page, captchaSelector, {
562
+ lang,
563
+ expectedLength,
564
+ allowedChars,
565
+ retries: maxRetries
566
+ });
567
+
568
+ notifyProgress('solve_captcha', result.success ? 'completed' : 'error',
569
+ result.success ? `OCR result: "${result.text}"` : 'OCR failed');
570
+
571
+ return {
572
+ success: result.success,
573
+ type: 'ocr',
574
+ text: result.text,
575
+ confidence: result.confidence,
576
+ attempts: result.attempts,
577
+ filled: false
578
+ };
579
+ }
580
+ } catch (err) {
581
+ notifyProgress('solve_captcha', 'error', `OCR error: ${err.message}`);
582
+ return { success: false, error: err.message, type: 'ocr' };
583
+ }
584
+ }
585
+
586
+ // Auto-detect captcha type
587
+ if (type === 'auto') {
588
+ // Check for text/image captcha first
589
+ const hasTextCaptcha = await page.evaluate(() => {
590
+ const captchaIndicators = [
591
+ 'img[src*="captcha"]',
592
+ 'img[alt*="captcha"]',
593
+ 'img[id*="captcha"]',
594
+ '.captcha-image',
595
+ '#captcha-image',
596
+ 'img[src*="Captcha"]'
597
+ ];
598
+ return captchaIndicators.some(sel => document.querySelector(sel));
599
+ });
600
+
601
+ if (hasTextCaptcha && captchaSelector) {
602
+ // Recursively call with text type
603
+ return this.solve_captcha({ ...params, type: 'text' });
604
+ }
605
+ }
606
+
607
+ // Original Turnstile/reCAPTCHA/hCaptcha handling
516
608
  const start = Date.now();
517
609
  let attempts = 0;
518
610
 
@@ -173,21 +173,34 @@ const TOOLS = [
173
173
  }
174
174
  },
175
175
 
176
- // 8. Solve Captcha
176
+ // 8. Solve Captcha (Enhanced with OCR for text captchas)
177
177
  {
178
178
  name: 'solve_captcha',
179
179
  emoji: '🔓',
180
- description: 'Auto-solve CAPTCHA with AI (Turnstile, reCAPTCHA, hCaptcha)',
181
- descriptionHindi: 'CAPTCHA हल करना (AI-powered)',
180
+ description: 'Auto-solve CAPTCHA with AI (Turnstile, reCAPTCHA, hCaptcha, Text/Image OCR)',
181
+ descriptionHindi: 'CAPTCHA हल करना (AI + OCR powered)',
182
182
  category: 'interaction',
183
183
  requiresBrowser: true,
184
184
  requiresPage: true,
185
185
  inputSchema: {
186
186
  type: 'object',
187
187
  properties: {
188
- type: { type: 'string', enum: ['turnstile', 'recaptcha', 'hcaptcha', 'auto'], default: 'auto' },
188
+ type: {
189
+ type: 'string',
190
+ enum: ['turnstile', 'recaptcha', 'hcaptcha', 'text', 'image', 'auto'],
191
+ default: 'auto',
192
+ description: 'Captcha type: turnstile/recaptcha/hcaptcha (JS-based), text/image (OCR-based), auto (detect)'
193
+ },
189
194
  timeout: { type: 'number', default: 30000 },
190
- aiMode: { type: 'boolean', default: true, description: 'Use AI vision for complex CAPTCHAs' }
195
+ aiMode: { type: 'boolean', default: true, description: 'Use AI vision for complex CAPTCHAs' },
196
+ // OCR-specific options for text/image captchas
197
+ captchaSelector: { type: 'string', description: 'CSS selector for captcha image (required for text/image type)' },
198
+ inputSelector: { type: 'string', description: 'CSS selector for input field to fill result' },
199
+ refreshSelector: { type: 'string', description: 'CSS selector for captcha refresh button' },
200
+ lang: { type: 'string', default: 'eng', description: 'OCR language: eng, hin, eng+hin' },
201
+ expectedLength: { type: 'number', description: 'Expected captcha text length' },
202
+ allowedChars: { type: 'string', description: 'Allowed characters in captcha' },
203
+ maxRetries: { type: 'number', default: 3, description: 'Max refresh attempts for OCR' }
191
204
  }
192
205
  }
193
206
  },