@juspay/neurolink 8.35.0 → 8.35.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [8.35.2](https://github.com/juspay/neurolink/compare/v8.35.1...v8.35.2) (2026-01-15)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(provider):** add network retry logic with exponential backoff to detection operations ([3b29e24](https://github.com/juspay/neurolink/commit/3b29e248766fac01f1e3ea368e9439233957121e))
6
+
7
+ ## [8.35.1](https://github.com/juspay/neurolink/compare/v8.35.0...v8.35.1) (2026-01-15)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(pdf):** reject empty PDFs with 0 pages instead of returning success ([92d8d4e](https://github.com/juspay/neurolink/commit/92d8d4eb3e6e675ee969e3a8ee92c5997dc416aa))
12
+
1
13
  ## [8.35.0](https://github.com/juspay/neurolink/compare/v8.34.1...v8.35.0) (2026-01-15)
2
14
 
3
15
  ### Features
@@ -225,6 +225,10 @@ export type FileDetectorOptions = {
225
225
  officeOptions?: OfficeProcessorOptions;
226
226
  confidenceThreshold?: number;
227
227
  provider?: string;
228
+ /** Maximum number of retry attempts for network requests (default: 3) */
229
+ maxRetries?: number;
230
+ /** Initial retry delay in milliseconds with exponential backoff (default: 1000) */
231
+ retryDelay?: number;
228
232
  };
229
233
  /**
230
234
  * Google AI Studio Files API types
@@ -70,7 +70,7 @@ export declare class FileDetector {
70
70
  */
71
71
  private static processFile;
72
72
  /**
73
- * Load file from URL
73
+ * Load file from URL with automatic retry on transient network errors
74
74
  */
75
75
  private static loadFromURL;
76
76
  /**
@@ -9,6 +9,116 @@ import { logger } from "./logger.js";
9
9
  import { CSVProcessor } from "./csvProcessor.js";
10
10
  import { ImageProcessor } from "./imageProcessor.js";
11
11
  import { PDFProcessor } from "./pdfProcessor.js";
12
+ /**
13
+ * Default retry configuration constants
14
+ */
15
+ const DEFAULT_MAX_RETRIES = 3;
16
+ const DEFAULT_RETRY_DELAY = 1000; // milliseconds
17
+ /**
18
+ * Retryable network error codes (Node.js/undici network errors)
19
+ */
20
+ const RETRYABLE_ERROR_CODES = [
21
+ "ETIMEDOUT",
22
+ "ECONNRESET",
23
+ "ECONNREFUSED",
24
+ "ENOTFOUND",
25
+ "ENETUNREACH",
26
+ "EAI_AGAIN",
27
+ "EPIPE",
28
+ "ECONNABORTED",
29
+ "UND_ERR_CONNECT_TIMEOUT",
30
+ "UND_ERR_HEADERS_TIMEOUT",
31
+ "UND_ERR_BODY_TIMEOUT",
32
+ "UND_ERR_SOCKET",
33
+ ];
34
+ /**
35
+ * Non-retryable HTTP status codes (client errors)
36
+ */
37
+ const NON_RETRYABLE_STATUS_CODES = [400, 401, 403, 404, 405];
38
+ /**
39
+ * Retryable HTTP status codes (server errors + rate limiting)
40
+ */
41
+ const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
42
+ /**
43
+ * Check if an error is a recoverable network error that should be retried
44
+ *
45
+ * @param error - Error to check
46
+ * @returns True if error is retryable (transient network issue)
47
+ */
48
+ function isRetryableNetworkError(error) {
49
+ if (!(error instanceof Error)) {
50
+ return false;
51
+ }
52
+ const errorMessage = error.message.toLowerCase();
53
+ // Extract error code from various error shapes
54
+ const errorWithCode = error;
55
+ const errorCode = errorWithCode.code?.toUpperCase();
56
+ // Check for retryable network error codes
57
+ if (errorCode && RETRYABLE_ERROR_CODES.includes(errorCode)) {
58
+ return true;
59
+ }
60
+ // Check HTTP status code if present in error message (e.g., "HTTP 503")
61
+ const httpStatusMatch = errorMessage.match(/http\s*(\d{3})/);
62
+ if (httpStatusMatch) {
63
+ const statusCode = parseInt(httpStatusMatch[1], 10);
64
+ if (NON_RETRYABLE_STATUS_CODES.includes(statusCode)) {
65
+ return false;
66
+ }
67
+ if (RETRYABLE_STATUS_CODES.includes(statusCode)) {
68
+ return true;
69
+ }
70
+ }
71
+ // Check error message for transient issues
72
+ const transientKeywords = [
73
+ "timeout",
74
+ "timed out",
75
+ "connection reset",
76
+ "econnreset",
77
+ "etimedout",
78
+ "network error",
79
+ "socket hang up",
80
+ "enotfound",
81
+ "getaddrinfo",
82
+ "unavailable",
83
+ "service unavailable",
84
+ ];
85
+ return transientKeywords.some((keyword) => errorMessage.includes(keyword));
86
+ }
87
+ /**
88
+ * Execute an operation with automatic retry logic on transient network errors
89
+ *
90
+ * @param operation - Async function to execute
91
+ * @param options - Retry configuration options
92
+ * @returns Promise resolving to the operation result
93
+ * @throws Error if all retry attempts fail or error is non-retryable
94
+ */
95
+ async function withRetry(operation, options = {}) {
96
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
97
+ const retryDelay = options.retryDelay ?? DEFAULT_RETRY_DELAY;
98
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
99
+ try {
100
+ return await operation();
101
+ }
102
+ catch (error) {
103
+ const isRetryable = isRetryableNetworkError(error);
104
+ const isLastAttempt = attempt === maxRetries;
105
+ if (!isRetryable || isLastAttempt) {
106
+ throw error;
107
+ }
108
+ // Calculate exponential backoff delay
109
+ const delay = retryDelay * Math.pow(2, attempt);
110
+ logger.debug("Retrying network operation after transient error", {
111
+ attempt: attempt + 1,
112
+ maxRetries,
113
+ delay,
114
+ error: error instanceof Error ? error.message : String(error),
115
+ });
116
+ await new Promise((resolve) => setTimeout(resolve, delay));
117
+ }
118
+ }
119
+ // TypeScript exhaustiveness check - should never reach here
120
+ throw new Error("Retry logic failed unexpectedly");
121
+ }
12
122
  /**
13
123
  * Check if text has JSON markers (starts with { or [ and ends with corresponding closing bracket)
14
124
  */
@@ -356,30 +466,34 @@ export class FileDetector {
356
466
  }
357
467
  }
358
468
  /**
359
- * Load file from URL
469
+ * Load file from URL with automatic retry on transient network errors
360
470
  */
361
471
  static async loadFromURL(url, options) {
362
472
  const maxSize = options?.maxSize || 10 * 1024 * 1024;
363
473
  const timeout = options?.timeout || this.DEFAULT_NETWORK_TIMEOUT;
364
- const response = await request(url, {
365
- dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
366
- method: "GET",
367
- headersTimeout: timeout,
368
- bodyTimeout: timeout,
369
- });
370
- if (response.statusCode !== 200) {
371
- throw new Error(`HTTP ${response.statusCode}`);
372
- }
373
- const chunks = [];
374
- let totalSize = 0;
375
- for await (const chunk of response.body) {
376
- totalSize += chunk.length;
377
- if (totalSize > maxSize) {
378
- throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
474
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
475
+ const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
476
+ return withRetry(async () => {
477
+ const response = await request(url, {
478
+ dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
479
+ method: "GET",
480
+ headersTimeout: timeout,
481
+ bodyTimeout: timeout,
482
+ });
483
+ if (response.statusCode !== 200) {
484
+ throw new Error(`HTTP ${response.statusCode}`);
379
485
  }
380
- chunks.push(chunk);
381
- }
382
- return Buffer.concat(chunks);
486
+ const chunks = [];
487
+ let totalSize = 0;
488
+ for await (const chunk of response.body) {
489
+ totalSize += chunk.length;
490
+ if (totalSize > maxSize) {
491
+ throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
492
+ }
493
+ chunks.push(chunk);
494
+ }
495
+ return Buffer.concat(chunks);
496
+ }, { maxRetries, retryDelay });
383
497
  }
384
498
  /**
385
499
  * Load file from filesystem path
@@ -309,6 +309,10 @@ export class PDFProcessor {
309
309
  base64Length: base64Image.length,
310
310
  });
311
311
  }
312
+ // Check for empty PDF (0 pages)
313
+ if (images.length === 0) {
314
+ throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
315
+ }
312
316
  const conversionTimeMs = Date.now() - startTime;
313
317
  logger.info("[PDF→Image] ✅ PDF conversion completed", {
314
318
  pageCount: images.length,
@@ -225,6 +225,10 @@ export type FileDetectorOptions = {
225
225
  officeOptions?: OfficeProcessorOptions;
226
226
  confidenceThreshold?: number;
227
227
  provider?: string;
228
+ /** Maximum number of retry attempts for network requests (default: 3) */
229
+ maxRetries?: number;
230
+ /** Initial retry delay in milliseconds with exponential backoff (default: 1000) */
231
+ retryDelay?: number;
228
232
  };
229
233
  /**
230
234
  * Google AI Studio Files API types
@@ -70,7 +70,7 @@ export declare class FileDetector {
70
70
  */
71
71
  private static processFile;
72
72
  /**
73
- * Load file from URL
73
+ * Load file from URL with automatic retry on transient network errors
74
74
  */
75
75
  private static loadFromURL;
76
76
  /**
@@ -9,6 +9,116 @@ import { logger } from "./logger.js";
9
9
  import { CSVProcessor } from "./csvProcessor.js";
10
10
  import { ImageProcessor } from "./imageProcessor.js";
11
11
  import { PDFProcessor } from "./pdfProcessor.js";
12
+ /**
13
+ * Default retry configuration constants
14
+ */
15
+ const DEFAULT_MAX_RETRIES = 3;
16
+ const DEFAULT_RETRY_DELAY = 1000; // milliseconds
17
+ /**
18
+ * Retryable network error codes (Node.js/undici network errors)
19
+ */
20
+ const RETRYABLE_ERROR_CODES = [
21
+ "ETIMEDOUT",
22
+ "ECONNRESET",
23
+ "ECONNREFUSED",
24
+ "ENOTFOUND",
25
+ "ENETUNREACH",
26
+ "EAI_AGAIN",
27
+ "EPIPE",
28
+ "ECONNABORTED",
29
+ "UND_ERR_CONNECT_TIMEOUT",
30
+ "UND_ERR_HEADERS_TIMEOUT",
31
+ "UND_ERR_BODY_TIMEOUT",
32
+ "UND_ERR_SOCKET",
33
+ ];
34
+ /**
35
+ * Non-retryable HTTP status codes (client errors)
36
+ */
37
+ const NON_RETRYABLE_STATUS_CODES = [400, 401, 403, 404, 405];
38
+ /**
39
+ * Retryable HTTP status codes (server errors + rate limiting)
40
+ */
41
+ const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
42
+ /**
43
+ * Check if an error is a recoverable network error that should be retried
44
+ *
45
+ * @param error - Error to check
46
+ * @returns True if error is retryable (transient network issue)
47
+ */
48
+ function isRetryableNetworkError(error) {
49
+ if (!(error instanceof Error)) {
50
+ return false;
51
+ }
52
+ const errorMessage = error.message.toLowerCase();
53
+ // Extract error code from various error shapes
54
+ const errorWithCode = error;
55
+ const errorCode = errorWithCode.code?.toUpperCase();
56
+ // Check for retryable network error codes
57
+ if (errorCode && RETRYABLE_ERROR_CODES.includes(errorCode)) {
58
+ return true;
59
+ }
60
+ // Check HTTP status code if present in error message (e.g., "HTTP 503")
61
+ const httpStatusMatch = errorMessage.match(/http\s*(\d{3})/);
62
+ if (httpStatusMatch) {
63
+ const statusCode = parseInt(httpStatusMatch[1], 10);
64
+ if (NON_RETRYABLE_STATUS_CODES.includes(statusCode)) {
65
+ return false;
66
+ }
67
+ if (RETRYABLE_STATUS_CODES.includes(statusCode)) {
68
+ return true;
69
+ }
70
+ }
71
+ // Check error message for transient issues
72
+ const transientKeywords = [
73
+ "timeout",
74
+ "timed out",
75
+ "connection reset",
76
+ "econnreset",
77
+ "etimedout",
78
+ "network error",
79
+ "socket hang up",
80
+ "enotfound",
81
+ "getaddrinfo",
82
+ "unavailable",
83
+ "service unavailable",
84
+ ];
85
+ return transientKeywords.some((keyword) => errorMessage.includes(keyword));
86
+ }
87
+ /**
88
+ * Execute an operation with automatic retry logic on transient network errors
89
+ *
90
+ * @param operation - Async function to execute
91
+ * @param options - Retry configuration options
92
+ * @returns Promise resolving to the operation result
93
+ * @throws Error if all retry attempts fail or error is non-retryable
94
+ */
95
+ async function withRetry(operation, options = {}) {
96
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
97
+ const retryDelay = options.retryDelay ?? DEFAULT_RETRY_DELAY;
98
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
99
+ try {
100
+ return await operation();
101
+ }
102
+ catch (error) {
103
+ const isRetryable = isRetryableNetworkError(error);
104
+ const isLastAttempt = attempt === maxRetries;
105
+ if (!isRetryable || isLastAttempt) {
106
+ throw error;
107
+ }
108
+ // Calculate exponential backoff delay
109
+ const delay = retryDelay * Math.pow(2, attempt);
110
+ logger.debug("Retrying network operation after transient error", {
111
+ attempt: attempt + 1,
112
+ maxRetries,
113
+ delay,
114
+ error: error instanceof Error ? error.message : String(error),
115
+ });
116
+ await new Promise((resolve) => setTimeout(resolve, delay));
117
+ }
118
+ }
119
+ // TypeScript exhaustiveness check - should never reach here
120
+ throw new Error("Retry logic failed unexpectedly");
121
+ }
12
122
  /**
13
123
  * Check if text has JSON markers (starts with { or [ and ends with corresponding closing bracket)
14
124
  */
@@ -356,30 +466,34 @@ export class FileDetector {
356
466
  }
357
467
  }
358
468
  /**
359
- * Load file from URL
469
+ * Load file from URL with automatic retry on transient network errors
360
470
  */
361
471
  static async loadFromURL(url, options) {
362
472
  const maxSize = options?.maxSize || 10 * 1024 * 1024;
363
473
  const timeout = options?.timeout || this.DEFAULT_NETWORK_TIMEOUT;
364
- const response = await request(url, {
365
- dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
366
- method: "GET",
367
- headersTimeout: timeout,
368
- bodyTimeout: timeout,
369
- });
370
- if (response.statusCode !== 200) {
371
- throw new Error(`HTTP ${response.statusCode}`);
372
- }
373
- const chunks = [];
374
- let totalSize = 0;
375
- for await (const chunk of response.body) {
376
- totalSize += chunk.length;
377
- if (totalSize > maxSize) {
378
- throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
474
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
475
+ const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
476
+ return withRetry(async () => {
477
+ const response = await request(url, {
478
+ dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
479
+ method: "GET",
480
+ headersTimeout: timeout,
481
+ bodyTimeout: timeout,
482
+ });
483
+ if (response.statusCode !== 200) {
484
+ throw new Error(`HTTP ${response.statusCode}`);
379
485
  }
380
- chunks.push(chunk);
381
- }
382
- return Buffer.concat(chunks);
486
+ const chunks = [];
487
+ let totalSize = 0;
488
+ for await (const chunk of response.body) {
489
+ totalSize += chunk.length;
490
+ if (totalSize > maxSize) {
491
+ throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
492
+ }
493
+ chunks.push(chunk);
494
+ }
495
+ return Buffer.concat(chunks);
496
+ }, { maxRetries, retryDelay });
383
497
  }
384
498
  /**
385
499
  * Load file from filesystem path
@@ -309,6 +309,10 @@ export class PDFProcessor {
309
309
  base64Length: base64Image.length,
310
310
  });
311
311
  }
312
+ // Check for empty PDF (0 pages)
313
+ if (images.length === 0) {
314
+ throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
315
+ }
312
316
  const conversionTimeMs = Date.now() - startTime;
313
317
  logger.info("[PDF→Image] ✅ PDF conversion completed", {
314
318
  pageCount: images.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "8.35.0",
3
+ "version": "8.35.2",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",