@juspay/neurolink 7.46.0 → 7.47.1
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 +12 -0
- package/dist/adapters/providerImageAdapter.js +12 -0
- package/dist/core/constants.js +1 -1
- package/dist/factories/providerRegistry.js +1 -1
- package/dist/lib/adapters/providerImageAdapter.js +12 -0
- package/dist/lib/core/constants.js +1 -1
- package/dist/lib/factories/providerRegistry.js +1 -1
- package/dist/lib/neurolink.d.ts +4 -0
- package/dist/lib/neurolink.js +30 -27
- package/dist/lib/providers/azureOpenai.js +36 -3
- package/dist/lib/providers/googleAiStudio.js +37 -3
- package/dist/lib/providers/googleVertex.js +37 -3
- package/dist/lib/utils/imageProcessor.d.ts +44 -0
- package/dist/lib/utils/imageProcessor.js +159 -8
- package/dist/lib/utils/messageBuilder.d.ts +4 -6
- package/dist/lib/utils/messageBuilder.js +145 -1
- package/dist/neurolink.d.ts +4 -0
- package/dist/neurolink.js +30 -27
- package/dist/providers/azureOpenai.js +36 -3
- package/dist/providers/googleAiStudio.js +37 -3
- package/dist/providers/googleVertex.js +37 -3
- package/dist/utils/imageProcessor.d.ts +44 -0
- package/dist/utils/imageProcessor.js +159 -8
- package/dist/utils/messageBuilder.d.ts +4 -6
- package/dist/utils/messageBuilder.js +145 -1
- package/package.json +1 -1
|
@@ -151,6 +151,8 @@ export class ImageProcessor {
|
|
|
151
151
|
bmp: "image/bmp",
|
|
152
152
|
tiff: "image/tiff",
|
|
153
153
|
tif: "image/tiff",
|
|
154
|
+
svg: "image/svg+xml",
|
|
155
|
+
avif: "image/avif",
|
|
154
156
|
};
|
|
155
157
|
return imageTypes[extension || ""] || "image/jpeg";
|
|
156
158
|
}
|
|
@@ -183,6 +185,21 @@ export class ImageProcessor {
|
|
|
183
185
|
return "image/webp";
|
|
184
186
|
}
|
|
185
187
|
}
|
|
188
|
+
// SVG: check for "<svg" or "<?xml" at start (text-based)
|
|
189
|
+
if (input.length >= 4) {
|
|
190
|
+
const start = input.subarray(0, 4).toString();
|
|
191
|
+
if (start === "<svg" || start === "<?xm") {
|
|
192
|
+
return "image/svg+xml";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// AVIF: check for "ftypavif" signature at bytes 4-11
|
|
196
|
+
if (input.length >= 12) {
|
|
197
|
+
const ftyp = input.subarray(4, 8).toString();
|
|
198
|
+
const brand = input.subarray(8, 12).toString();
|
|
199
|
+
if (ftyp === "ftyp" && brand === "avif") {
|
|
200
|
+
return "image/avif";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
186
203
|
}
|
|
187
204
|
return "image/jpeg"; // Default fallback
|
|
188
205
|
}
|
|
@@ -217,6 +234,8 @@ export class ImageProcessor {
|
|
|
217
234
|
"image/webp",
|
|
218
235
|
"image/bmp",
|
|
219
236
|
"image/tiff",
|
|
237
|
+
"image/svg+xml",
|
|
238
|
+
"image/avif",
|
|
220
239
|
];
|
|
221
240
|
return supportedFormats.includes(mediaType.toLowerCase());
|
|
222
241
|
}
|
|
@@ -332,14 +351,7 @@ export const imageUtils = {
|
|
|
332
351
|
/**
|
|
333
352
|
* Check if a string is base64 encoded
|
|
334
353
|
*/
|
|
335
|
-
isBase64: (str) =>
|
|
336
|
-
try {
|
|
337
|
-
return btoa(atob(str)) === str;
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
},
|
|
354
|
+
isBase64: (str) => imageUtils.isValidBase64(str),
|
|
343
355
|
/**
|
|
344
356
|
* Extract file extension from filename or URL
|
|
345
357
|
*/
|
|
@@ -359,4 +371,143 @@ export const imageUtils = {
|
|
|
359
371
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
360
372
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
361
373
|
},
|
|
374
|
+
/**
|
|
375
|
+
* Convert Buffer to base64 string
|
|
376
|
+
*/
|
|
377
|
+
bufferToBase64: (buffer) => {
|
|
378
|
+
return buffer.toString("base64");
|
|
379
|
+
},
|
|
380
|
+
/**
|
|
381
|
+
* Convert base64 string to Buffer
|
|
382
|
+
*/
|
|
383
|
+
base64ToBuffer: (base64) => {
|
|
384
|
+
// Remove data URI prefix if present
|
|
385
|
+
const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
386
|
+
return Buffer.from(cleanBase64, "base64");
|
|
387
|
+
},
|
|
388
|
+
/**
|
|
389
|
+
* Convert file path to base64 data URI
|
|
390
|
+
*/
|
|
391
|
+
fileToBase64DataUri: async (filePath, maxBytes = 10 * 1024 * 1024) => {
|
|
392
|
+
try {
|
|
393
|
+
const fs = await import("fs/promises");
|
|
394
|
+
// File existence and type validation
|
|
395
|
+
const stat = await fs.stat(filePath);
|
|
396
|
+
if (!stat.isFile()) {
|
|
397
|
+
throw new Error("Not a file");
|
|
398
|
+
}
|
|
399
|
+
// Size check before reading - prevent memory exhaustion
|
|
400
|
+
if (stat.size > maxBytes) {
|
|
401
|
+
throw new Error(`File too large: ${stat.size} bytes (max: ${maxBytes} bytes)`);
|
|
402
|
+
}
|
|
403
|
+
const buffer = await fs.readFile(filePath);
|
|
404
|
+
// Enhanced MIME detection: try buffer content first, fallback to filename
|
|
405
|
+
const mimeType = ImageProcessor.detectImageType(buffer) ||
|
|
406
|
+
ImageProcessor.detectImageType(filePath);
|
|
407
|
+
const base64 = buffer.toString("base64");
|
|
408
|
+
return `data:${mimeType};base64,${base64}`;
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
throw new Error(`Failed to convert file to base64: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
/**
|
|
415
|
+
* Convert URL to base64 data URI by downloading the image
|
|
416
|
+
*/
|
|
417
|
+
urlToBase64DataUri: async (url, { timeoutMs = 15000, maxBytes = 10 * 1024 * 1024 } = {}) => {
|
|
418
|
+
try {
|
|
419
|
+
// Basic protocol whitelist
|
|
420
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
421
|
+
throw new Error("Unsupported protocol");
|
|
422
|
+
}
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
425
|
+
try {
|
|
426
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
429
|
+
}
|
|
430
|
+
const contentType = response.headers.get("content-type") || "";
|
|
431
|
+
if (!/^image\//i.test(contentType)) {
|
|
432
|
+
throw new Error(`Unsupported content-type: ${contentType || "unknown"}`);
|
|
433
|
+
}
|
|
434
|
+
const len = Number(response.headers.get("content-length") || 0);
|
|
435
|
+
if (len && len > maxBytes) {
|
|
436
|
+
throw new Error(`Content too large: ${len} bytes`);
|
|
437
|
+
}
|
|
438
|
+
const buffer = await response.arrayBuffer();
|
|
439
|
+
if (buffer.byteLength > maxBytes) {
|
|
440
|
+
throw new Error(`Downloaded content too large: ${buffer.byteLength} bytes`);
|
|
441
|
+
}
|
|
442
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
443
|
+
return `data:${contentType || "image/jpeg"};base64,${base64}`;
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
clearTimeout(t);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
throw new Error(`Failed to download and convert URL to base64: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
/**
|
|
454
|
+
* Extract base64 data from data URI
|
|
455
|
+
*/
|
|
456
|
+
extractBase64FromDataUri: (dataUri) => {
|
|
457
|
+
if (!dataUri.includes(",")) {
|
|
458
|
+
return dataUri; // Already just base64
|
|
459
|
+
}
|
|
460
|
+
return dataUri.split(",")[1];
|
|
461
|
+
},
|
|
462
|
+
/**
|
|
463
|
+
* Extract MIME type from data URI
|
|
464
|
+
*/
|
|
465
|
+
extractMimeTypeFromDataUri: (dataUri) => {
|
|
466
|
+
const match = dataUri.match(/^data:([^;]+);base64,/);
|
|
467
|
+
return match ? match[1] : "image/jpeg";
|
|
468
|
+
},
|
|
469
|
+
/**
|
|
470
|
+
* Create data URI from base64 and MIME type
|
|
471
|
+
*/
|
|
472
|
+
createDataUri: (base64, mimeType = "image/jpeg") => {
|
|
473
|
+
// Remove data URI prefix if already present
|
|
474
|
+
const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
475
|
+
return `data:${mimeType};base64,${cleanBase64}`;
|
|
476
|
+
},
|
|
477
|
+
/**
|
|
478
|
+
* Validate base64 string format
|
|
479
|
+
*/
|
|
480
|
+
isValidBase64: (str) => {
|
|
481
|
+
try {
|
|
482
|
+
// Remove data URI prefix if present
|
|
483
|
+
const cleanBase64 = str.includes(",") ? str.split(",")[1] : str;
|
|
484
|
+
// Check if it's valid base64
|
|
485
|
+
const decoded = Buffer.from(cleanBase64, "base64");
|
|
486
|
+
const reencoded = decoded.toString("base64");
|
|
487
|
+
// Remove padding for comparison (base64 can have different padding)
|
|
488
|
+
const normalizeBase64 = (b64) => b64.replace(/=+$/, "");
|
|
489
|
+
return normalizeBase64(cleanBase64) === normalizeBase64(reencoded);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
/**
|
|
496
|
+
* Get base64 string size in bytes
|
|
497
|
+
*/
|
|
498
|
+
getBase64Size: (base64) => {
|
|
499
|
+
// Remove data URI prefix if present
|
|
500
|
+
const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
501
|
+
return Buffer.byteLength(cleanBase64, "base64");
|
|
502
|
+
},
|
|
503
|
+
/**
|
|
504
|
+
* Compress base64 image by reducing quality (basic implementation)
|
|
505
|
+
* Note: This is a placeholder - for production use, consider using sharp or similar
|
|
506
|
+
*/
|
|
507
|
+
compressBase64: (base64, _quality = 0.8) => {
|
|
508
|
+
// This is a basic implementation that just returns the original
|
|
509
|
+
// In a real implementation, you'd use an image processing library
|
|
510
|
+
logger.warn("Base64 compression not implemented - returning original");
|
|
511
|
+
return base64;
|
|
512
|
+
},
|
|
362
513
|
};
|
|
@@ -7,13 +7,12 @@ import type { MultimodalChatMessage } from "../types/conversation.js";
|
|
|
7
7
|
import type { TextGenerationOptions } from "../types/index.js";
|
|
8
8
|
import type { StreamOptions } from "../types/streamTypes.js";
|
|
9
9
|
import type { GenerateOptions } from "../types/generateTypes.js";
|
|
10
|
+
import type { CoreMessage } from "ai";
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Type-safe conversion from MultimodalChatMessage[] to CoreMessage[]
|
|
13
|
+
* Filters out invalid content and ensures strict CoreMessage contract compliance
|
|
12
14
|
*/
|
|
13
|
-
|
|
14
|
-
role: "user" | "assistant" | "system";
|
|
15
|
-
content: string;
|
|
16
|
-
};
|
|
15
|
+
export declare function convertToCoreMessages(messages: MultimodalChatMessage[]): CoreMessage[];
|
|
17
16
|
/**
|
|
18
17
|
* Build a properly formatted message array for AI providers
|
|
19
18
|
* Combines system prompt, conversation history, and current user prompt
|
|
@@ -25,4 +24,3 @@ export declare function buildMessagesArray(options: TextGenerationOptions | Stre
|
|
|
25
24
|
* Detects when images are present and routes through provider adapter
|
|
26
25
|
*/
|
|
27
26
|
export declare function buildMultimodalMessagesArray(options: GenerateOptions, provider: string, model: string): Promise<MultimodalChatMessage[]>;
|
|
28
|
-
export {};
|
|
@@ -8,6 +8,147 @@ import { ProviderImageAdapter, MultimodalLogger, } from "../adapters/providerIma
|
|
|
8
8
|
import { logger } from "./logger.js";
|
|
9
9
|
import { request } from "undici";
|
|
10
10
|
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
/**
|
|
12
|
+
* Type guard for validating message roles
|
|
13
|
+
*/
|
|
14
|
+
function isValidRole(role) {
|
|
15
|
+
return (typeof role === "string" &&
|
|
16
|
+
(role === "user" || role === "assistant" || role === "system"));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Type guard for validating content items
|
|
20
|
+
*/
|
|
21
|
+
function isValidContentItem(item) {
|
|
22
|
+
if (!item || typeof item !== "object") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const contentItem = item;
|
|
26
|
+
if (contentItem.type === "text") {
|
|
27
|
+
return typeof contentItem.text === "string";
|
|
28
|
+
}
|
|
29
|
+
if (contentItem.type === "image") {
|
|
30
|
+
return (typeof contentItem.image === "string" &&
|
|
31
|
+
(contentItem.mimeType === undefined ||
|
|
32
|
+
typeof contentItem.mimeType === "string"));
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Safely convert content item to AI SDK content format
|
|
38
|
+
*/
|
|
39
|
+
function convertContentItem(item) {
|
|
40
|
+
if (!isValidContentItem(item)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const contentItem = item;
|
|
44
|
+
if (contentItem.type === "text" && typeof contentItem.text === "string") {
|
|
45
|
+
return { type: "text", text: contentItem.text };
|
|
46
|
+
}
|
|
47
|
+
if (contentItem.type === "image" && typeof contentItem.image === "string") {
|
|
48
|
+
return {
|
|
49
|
+
type: "image",
|
|
50
|
+
image: contentItem.image,
|
|
51
|
+
...(contentItem.mimeType && { mimeType: contentItem.mimeType }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Type-safe conversion from MultimodalChatMessage[] to CoreMessage[]
|
|
58
|
+
* Filters out invalid content and ensures strict CoreMessage contract compliance
|
|
59
|
+
*/
|
|
60
|
+
export function convertToCoreMessages(messages) {
|
|
61
|
+
return messages
|
|
62
|
+
.map((msg) => {
|
|
63
|
+
// Validate role
|
|
64
|
+
if (!isValidRole(msg.role)) {
|
|
65
|
+
logger.warn("Invalid message role found, skipping", { role: msg.role });
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
// Handle string content
|
|
69
|
+
if (typeof msg.content === "string") {
|
|
70
|
+
// Create properly typed discriminated union messages
|
|
71
|
+
if (msg.role === "system") {
|
|
72
|
+
return {
|
|
73
|
+
role: "system",
|
|
74
|
+
content: msg.content,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else if (msg.role === "user") {
|
|
78
|
+
return {
|
|
79
|
+
role: "user",
|
|
80
|
+
content: msg.content,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
else if (msg.role === "assistant") {
|
|
84
|
+
return {
|
|
85
|
+
role: "assistant",
|
|
86
|
+
content: msg.content,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Handle array content (multimodal) - only user messages support full multimodal content
|
|
91
|
+
if (Array.isArray(msg.content)) {
|
|
92
|
+
const validContent = msg.content
|
|
93
|
+
.map(convertContentItem)
|
|
94
|
+
.filter((item) => item !== null);
|
|
95
|
+
// If no valid content items, skip the message
|
|
96
|
+
if (validContent.length === 0) {
|
|
97
|
+
logger.warn("No valid content items found in multimodal message, skipping");
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (msg.role === "user") {
|
|
101
|
+
// User messages support both text and image content
|
|
102
|
+
return {
|
|
103
|
+
role: "user",
|
|
104
|
+
content: validContent,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else if (msg.role === "assistant") {
|
|
108
|
+
// Assistant messages only support text content, filter out images
|
|
109
|
+
const textOnlyContent = validContent.filter((item) => item.type === "text");
|
|
110
|
+
if (textOnlyContent.length === 0) {
|
|
111
|
+
// If no text content, convert to empty string
|
|
112
|
+
return {
|
|
113
|
+
role: "assistant",
|
|
114
|
+
content: "",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
else if (textOnlyContent.length === 1) {
|
|
118
|
+
// Single text item, use string content
|
|
119
|
+
return {
|
|
120
|
+
role: "assistant",
|
|
121
|
+
content: textOnlyContent[0].text,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Multiple text items, concatenate them
|
|
126
|
+
const combinedText = textOnlyContent
|
|
127
|
+
.map((item) => item.text)
|
|
128
|
+
.join(" ");
|
|
129
|
+
return {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: combinedText,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// System messages cannot have multimodal content, convert to text
|
|
137
|
+
const textContent = validContent.find((item) => item.type === "text")?.text || "";
|
|
138
|
+
return {
|
|
139
|
+
role: "system",
|
|
140
|
+
content: textContent,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Invalid content type
|
|
145
|
+
logger.warn("Invalid message content type found, skipping", {
|
|
146
|
+
contentType: typeof msg.content,
|
|
147
|
+
});
|
|
148
|
+
return null;
|
|
149
|
+
})
|
|
150
|
+
.filter((msg) => msg !== null);
|
|
151
|
+
}
|
|
11
152
|
/**
|
|
12
153
|
* Convert ChatMessage to CoreMessage for AI SDK compatibility
|
|
13
154
|
*/
|
|
@@ -84,7 +225,10 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
84
225
|
// If no images, use standard message building and convert to MultimodalChatMessage[]
|
|
85
226
|
if (!hasImages) {
|
|
86
227
|
const standardMessages = buildMessagesArray(options);
|
|
87
|
-
return standardMessages.map((msg) => ({
|
|
228
|
+
return standardMessages.map((msg) => ({
|
|
229
|
+
role: msg.role,
|
|
230
|
+
content: typeof msg.content === "string" ? msg.content : msg.content,
|
|
231
|
+
}));
|
|
88
232
|
}
|
|
89
233
|
// Validate provider supports vision
|
|
90
234
|
if (!ProviderImageAdapter.supportsVision(provider, model)) {
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Uses real MCP infrastructure for tool discovery and execution.
|
|
7
7
|
*/
|
|
8
8
|
import type { TextGenerationOptions, TextGenerationResult } from "./types/index.js";
|
|
9
|
+
import { MCPToolRegistry } from "./mcp/toolRegistry.js";
|
|
9
10
|
import type { GenerateOptions, GenerateResult } from "./types/generateTypes.js";
|
|
10
11
|
import type { StreamOptions, StreamResult } from "./types/streamTypes.js";
|
|
11
12
|
import type { MCPServerInfo, MCPExecutableTool } from "./types/mcpTypes.js";
|
|
@@ -46,6 +47,7 @@ export interface MCPStatus {
|
|
|
46
47
|
export declare class NeuroLink {
|
|
47
48
|
private mcpInitialized;
|
|
48
49
|
private emitter;
|
|
50
|
+
private toolRegistry;
|
|
49
51
|
private autoDiscoveredServerInfos;
|
|
50
52
|
private externalServerManager;
|
|
51
53
|
private toolCache;
|
|
@@ -99,6 +101,7 @@ export declare class NeuroLink {
|
|
|
99
101
|
* @param config.hitl.dangerousActions - Keywords that trigger confirmation (default: ['delete', 'remove', 'drop'])
|
|
100
102
|
* @param config.hitl.timeout - Confirmation timeout in milliseconds (default: 30000)
|
|
101
103
|
* @param config.hitl.allowArgumentModification - Allow users to modify tool parameters (default: true)
|
|
104
|
+
* @param config.toolRegistry - Optional tool registry instance for advanced use cases (default: new MCPToolRegistry())
|
|
102
105
|
*
|
|
103
106
|
* @example
|
|
104
107
|
* ```typescript
|
|
@@ -139,6 +142,7 @@ export declare class NeuroLink {
|
|
|
139
142
|
conversationMemory?: Partial<ConversationMemoryConfig>;
|
|
140
143
|
enableOrchestration?: boolean;
|
|
141
144
|
hitl?: HITLConfig;
|
|
145
|
+
toolRegistry?: MCPToolRegistry;
|
|
142
146
|
});
|
|
143
147
|
/**
|
|
144
148
|
* Initialize provider registry with security settings
|
package/dist/neurolink.js
CHANGED
|
@@ -18,7 +18,7 @@ import { mcpLogger } from "./utils/logger.js";
|
|
|
18
18
|
import { SYSTEM_LIMITS } from "./core/constants.js";
|
|
19
19
|
import { NANOSECOND_TO_MS_DIVISOR, TOOL_TIMEOUTS, RETRY_ATTEMPTS, RETRY_DELAYS, CIRCUIT_BREAKER, CIRCUIT_BREAKER_RESET_MS, MEMORY_THRESHOLDS, PROVIDER_TIMEOUTS, PERFORMANCE_THRESHOLDS, } from "./constants/index.js";
|
|
20
20
|
import pLimit from "p-limit";
|
|
21
|
-
import {
|
|
21
|
+
import { MCPToolRegistry } from "./mcp/toolRegistry.js";
|
|
22
22
|
import { logger } from "./utils/logger.js";
|
|
23
23
|
import { getBestProvider } from "./utils/providerUtils.js";
|
|
24
24
|
import { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
@@ -45,6 +45,7 @@ import { isZodSchema } from "./utils/schemaConversion.js";
|
|
|
45
45
|
export class NeuroLink {
|
|
46
46
|
mcpInitialized = false;
|
|
47
47
|
emitter = new EventEmitter();
|
|
48
|
+
toolRegistry;
|
|
48
49
|
autoDiscoveredServerInfos = [];
|
|
49
50
|
// External MCP server management
|
|
50
51
|
externalServerManager;
|
|
@@ -140,6 +141,7 @@ export class NeuroLink {
|
|
|
140
141
|
* @param config.hitl.dangerousActions - Keywords that trigger confirmation (default: ['delete', 'remove', 'drop'])
|
|
141
142
|
* @param config.hitl.timeout - Confirmation timeout in milliseconds (default: 30000)
|
|
142
143
|
* @param config.hitl.allowArgumentModification - Allow users to modify tool parameters (default: true)
|
|
144
|
+
* @param config.toolRegistry - Optional tool registry instance for advanced use cases (default: new MCPToolRegistry())
|
|
143
145
|
*
|
|
144
146
|
* @example
|
|
145
147
|
* ```typescript
|
|
@@ -177,6 +179,7 @@ export class NeuroLink {
|
|
|
177
179
|
* @throws {Error} When HITL configuration is invalid (if enabled)
|
|
178
180
|
*/
|
|
179
181
|
constructor(config) {
|
|
182
|
+
this.toolRegistry = config?.toolRegistry || new MCPToolRegistry();
|
|
180
183
|
// Initialize orchestration setting
|
|
181
184
|
this.enableOrchestration = config?.enableOrchestration ?? false;
|
|
182
185
|
// Read tool cache duration from environment variables, with a default
|
|
@@ -278,7 +281,7 @@ export class NeuroLink {
|
|
|
278
281
|
// Initialize HITL manager
|
|
279
282
|
this.hitlManager = new HITLManager(config.hitl);
|
|
280
283
|
// Inject HITL manager into tool registry
|
|
281
|
-
toolRegistry.setHITLManager(this.hitlManager);
|
|
284
|
+
this.toolRegistry.setHITLManager(this.hitlManager);
|
|
282
285
|
// Inject HITL manager into external server manager
|
|
283
286
|
this.externalServerManager.setHITLManager(this.hitlManager);
|
|
284
287
|
// Set up HITL event forwarding to main emitter
|
|
@@ -627,7 +630,7 @@ export class NeuroLink {
|
|
|
627
630
|
mcpLogger.debug("Direct tools server are disabled via environment variable.");
|
|
628
631
|
}
|
|
629
632
|
else {
|
|
630
|
-
await toolRegistry.registerServer("neurolink-direct", directToolsServer);
|
|
633
|
+
await this.toolRegistry.registerServer("neurolink-direct", directToolsServer);
|
|
631
634
|
mcpLogger.debug("[NeuroLink] Direct tools server registered successfully", {
|
|
632
635
|
serverId: "neurolink-direct",
|
|
633
636
|
});
|
|
@@ -1371,7 +1374,7 @@ export class NeuroLink {
|
|
|
1371
1374
|
mcpInitialized: this.mcpInitialized,
|
|
1372
1375
|
mcpComponents: {
|
|
1373
1376
|
hasExternalServerManager: !!this.externalServerManager,
|
|
1374
|
-
hasToolRegistry: !!toolRegistry,
|
|
1377
|
+
hasToolRegistry: !!this.toolRegistry,
|
|
1375
1378
|
hasProviderRegistry: !!AIProviderFactory,
|
|
1376
1379
|
},
|
|
1377
1380
|
fallbackReason: "MCP_NOT_INITIALIZED",
|
|
@@ -2403,7 +2406,7 @@ export class NeuroLink {
|
|
|
2403
2406
|
// SMART DEFAULTS: Use utility to eliminate boilerplate creation
|
|
2404
2407
|
const mcpServerInfo = createCustomToolServerInfo(name, convertedTool);
|
|
2405
2408
|
// Register with toolRegistry using MCPServerInfo directly
|
|
2406
|
-
toolRegistry.registerServer(mcpServerInfo);
|
|
2409
|
+
this.toolRegistry.registerServer(mcpServerInfo);
|
|
2407
2410
|
// Emit tool registration success event
|
|
2408
2411
|
this.emitter.emit("tools-register:end", {
|
|
2409
2412
|
toolName: name,
|
|
@@ -2475,7 +2478,7 @@ export class NeuroLink {
|
|
|
2475
2478
|
unregisterTool(name) {
|
|
2476
2479
|
this.invalidateToolCache(); // Invalidate cache when a tool is unregistered
|
|
2477
2480
|
const serverId = `custom-tool-${name}`;
|
|
2478
|
-
const removed = toolRegistry.unregisterServer(serverId);
|
|
2481
|
+
const removed = this.toolRegistry.unregisterServer(serverId);
|
|
2479
2482
|
if (removed) {
|
|
2480
2483
|
logger.info(`Unregistered custom tool: ${name}`);
|
|
2481
2484
|
}
|
|
@@ -2487,7 +2490,7 @@ export class NeuroLink {
|
|
|
2487
2490
|
*/
|
|
2488
2491
|
getCustomTools() {
|
|
2489
2492
|
// Get tools from toolRegistry with smart category detection
|
|
2490
|
-
const customTools = toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true }));
|
|
2493
|
+
const customTools = this.toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true }));
|
|
2491
2494
|
const toolMap = new Map();
|
|
2492
2495
|
for (const tool of customTools) {
|
|
2493
2496
|
const effectiveSchema = tool.inputSchema || tool.parameters;
|
|
@@ -2545,7 +2548,7 @@ export class NeuroLink {
|
|
|
2545
2548
|
hasShopId: !!executionContext.shopId,
|
|
2546
2549
|
sessionId: executionContext.sessionId,
|
|
2547
2550
|
});
|
|
2548
|
-
return await toolRegistry.executeTool(tool.name, params, executionContext);
|
|
2551
|
+
return await this.toolRegistry.executeTool(tool.name, params, executionContext);
|
|
2549
2552
|
},
|
|
2550
2553
|
});
|
|
2551
2554
|
}
|
|
@@ -2566,7 +2569,7 @@ export class NeuroLink {
|
|
|
2566
2569
|
serverInfo.tools = [];
|
|
2567
2570
|
}
|
|
2568
2571
|
// ZERO CONVERSIONS: Pass MCPServerInfo directly to toolRegistry
|
|
2569
|
-
await toolRegistry.registerServer(serverInfo);
|
|
2572
|
+
await this.toolRegistry.registerServer(serverInfo);
|
|
2570
2573
|
mcpLogger.info(`[NeuroLink] Successfully registered in-memory server: ${serverId}`, {
|
|
2571
2574
|
category: serverInfo.metadata?.category,
|
|
2572
2575
|
provider: serverInfo.metadata?.provider,
|
|
@@ -2584,7 +2587,7 @@ export class NeuroLink {
|
|
|
2584
2587
|
*/
|
|
2585
2588
|
getInMemoryServers() {
|
|
2586
2589
|
// Get in-memory servers from toolRegistry
|
|
2587
|
-
const serverInfos = toolRegistry.getBuiltInServerInfos();
|
|
2590
|
+
const serverInfos = this.toolRegistry.getBuiltInServerInfos();
|
|
2588
2591
|
const serverMap = new Map();
|
|
2589
2592
|
for (const serverInfo of serverInfos) {
|
|
2590
2593
|
if (detectCategory({
|
|
@@ -2603,7 +2606,7 @@ export class NeuroLink {
|
|
|
2603
2606
|
*/
|
|
2604
2607
|
getInMemoryServerInfos() {
|
|
2605
2608
|
// Get in-memory servers from centralized tool registry
|
|
2606
|
-
const allServers = toolRegistry.getBuiltInServerInfos();
|
|
2609
|
+
const allServers = this.toolRegistry.getBuiltInServerInfos();
|
|
2607
2610
|
return allServers.filter((server) => detectCategory({
|
|
2608
2611
|
existingCategory: server.metadata?.category,
|
|
2609
2612
|
serverId: server.id,
|
|
@@ -2855,7 +2858,7 @@ export class NeuroLink {
|
|
|
2855
2858
|
storedContextKeys: Object.keys(storedContext),
|
|
2856
2859
|
finalContextKeys: Object.keys(context),
|
|
2857
2860
|
});
|
|
2858
|
-
const result = (await toolRegistry.executeTool(toolName, params, context));
|
|
2861
|
+
const result = (await this.toolRegistry.executeTool(toolName, params, context));
|
|
2859
2862
|
// ADD: Check if result indicates a failure and emit error event
|
|
2860
2863
|
if (result &&
|
|
2861
2864
|
typeof result === "object" &&
|
|
@@ -2905,9 +2908,9 @@ export class NeuroLink {
|
|
|
2905
2908
|
getAllToolsHrTimeStart: getAllToolsHrTimeStart.toString(),
|
|
2906
2909
|
// 🔧 Tool registry state
|
|
2907
2910
|
toolRegistryState: {
|
|
2908
|
-
hasToolRegistry: !!toolRegistry,
|
|
2911
|
+
hasToolRegistry: !!this.toolRegistry,
|
|
2909
2912
|
toolRegistrySize: 0, // Not accessible as size property
|
|
2910
|
-
toolRegistryType: toolRegistry?.constructor?.name || "NOT_SET",
|
|
2913
|
+
toolRegistryType: this.toolRegistry?.constructor?.name || "NOT_SET",
|
|
2911
2914
|
hasExternalServerManager: !!this.externalServerManager,
|
|
2912
2915
|
externalServerManagerType: this.externalServerManager?.constructor?.name || "NOT_SET",
|
|
2913
2916
|
},
|
|
@@ -2926,7 +2929,7 @@ export class NeuroLink {
|
|
|
2926
2929
|
// Optimized: Collect all tools with minimal object creation
|
|
2927
2930
|
const allTools = new Map();
|
|
2928
2931
|
// 1. Add MCP server tools (built-in direct tools)
|
|
2929
|
-
const mcpToolsRaw = await toolRegistry.listTools();
|
|
2932
|
+
const mcpToolsRaw = await this.toolRegistry.listTools();
|
|
2930
2933
|
for (const tool of mcpToolsRaw) {
|
|
2931
2934
|
if (!allTools.has(tool.name)) {
|
|
2932
2935
|
const optimizedTool = optimizeToolForCollection(tool, {
|
|
@@ -2936,7 +2939,7 @@ export class NeuroLink {
|
|
|
2936
2939
|
}
|
|
2937
2940
|
}
|
|
2938
2941
|
// 2. Add custom tools from this NeuroLink instance
|
|
2939
|
-
const customToolsRaw = toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true }));
|
|
2942
|
+
const customToolsRaw = this.toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true }));
|
|
2940
2943
|
for (const tool of customToolsRaw) {
|
|
2941
2944
|
if (!allTools.has(tool.name)) {
|
|
2942
2945
|
const optimizedTool = optimizeToolForCollection(tool, {
|
|
@@ -2952,7 +2955,7 @@ export class NeuroLink {
|
|
|
2952
2955
|
}
|
|
2953
2956
|
}
|
|
2954
2957
|
// 3. Add tools from in-memory MCP servers
|
|
2955
|
-
const inMemoryToolsRaw = toolRegistry.getToolsByCategory("in-memory");
|
|
2958
|
+
const inMemoryToolsRaw = this.toolRegistry.getToolsByCategory("in-memory");
|
|
2956
2959
|
for (const tool of inMemoryToolsRaw) {
|
|
2957
2960
|
if (!allTools.has(tool.name)) {
|
|
2958
2961
|
const optimizedTool = optimizeToolForCollection(tool, {
|
|
@@ -3231,13 +3234,13 @@ export class NeuroLink {
|
|
|
3231
3234
|
// Initialize MCP if not already initialized (loads external servers from config)
|
|
3232
3235
|
await this.initializeMCP();
|
|
3233
3236
|
// Get built-in tools
|
|
3234
|
-
const allTools = await toolRegistry.listTools();
|
|
3237
|
+
const allTools = await this.toolRegistry.listTools();
|
|
3235
3238
|
// Get external MCP server statistics
|
|
3236
3239
|
const externalStats = this.externalServerManager.getStatistics();
|
|
3237
3240
|
// DIRECT RETURNS - ZERO conversion
|
|
3238
3241
|
const externalMCPServers = this.externalServerManager.listServers();
|
|
3239
3242
|
const inMemoryServerInfos = this.getInMemoryServerInfos();
|
|
3240
|
-
const builtInServerInfos = toolRegistry.getBuiltInServerInfos();
|
|
3243
|
+
const builtInServerInfos = this.toolRegistry.getBuiltInServerInfos();
|
|
3241
3244
|
const autoDiscoveredServerInfos = this.getAutoDiscoveredServerInfos();
|
|
3242
3245
|
// Calculate totals
|
|
3243
3246
|
const totalServers = externalMCPServers.length +
|
|
@@ -3255,7 +3258,7 @@ export class NeuroLink {
|
|
|
3255
3258
|
autoDiscoveredCount: autoDiscoveredServerInfos.length,
|
|
3256
3259
|
totalTools,
|
|
3257
3260
|
autoDiscoveredServers: autoDiscoveredServerInfos,
|
|
3258
|
-
customToolsCount: toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true })).length,
|
|
3261
|
+
customToolsCount: this.toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true })).length,
|
|
3259
3262
|
inMemoryServersCount: inMemoryServerInfos.length,
|
|
3260
3263
|
externalMCPServersCount: externalMCPServers.length,
|
|
3261
3264
|
externalMCPConnectedCount: externalStats.connectedServers,
|
|
@@ -3271,7 +3274,7 @@ export class NeuroLink {
|
|
|
3271
3274
|
autoDiscoveredCount: 0,
|
|
3272
3275
|
totalTools: 0,
|
|
3273
3276
|
autoDiscoveredServers: [],
|
|
3274
|
-
customToolsCount: toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true })).length,
|
|
3277
|
+
customToolsCount: this.toolRegistry.getToolsByCategory(detectCategory({ isCustomTool: true })).length,
|
|
3275
3278
|
inMemoryServersCount: 0,
|
|
3276
3279
|
externalMCPServersCount: 0,
|
|
3277
3280
|
externalMCPConnectedCount: 0,
|
|
@@ -3290,7 +3293,7 @@ export class NeuroLink {
|
|
|
3290
3293
|
return [
|
|
3291
3294
|
...this.externalServerManager.listServers(), // Direct return
|
|
3292
3295
|
...this.getInMemoryServerInfos(), // Direct return
|
|
3293
|
-
...toolRegistry.getBuiltInServerInfos(), // Direct return
|
|
3296
|
+
...this.toolRegistry.getBuiltInServerInfos(), // Direct return
|
|
3294
3297
|
...this.getAutoDiscoveredServerInfos(), // Direct return
|
|
3295
3298
|
];
|
|
3296
3299
|
}
|
|
@@ -3303,7 +3306,7 @@ export class NeuroLink {
|
|
|
3303
3306
|
try {
|
|
3304
3307
|
// Test built-in tools
|
|
3305
3308
|
if (serverId === "neurolink-direct") {
|
|
3306
|
-
const tools = await toolRegistry.listTools();
|
|
3309
|
+
const tools = await this.toolRegistry.listTools();
|
|
3307
3310
|
return tools.length > 0;
|
|
3308
3311
|
}
|
|
3309
3312
|
// Test in-memory servers
|
|
@@ -3480,7 +3483,7 @@ export class NeuroLink {
|
|
|
3480
3483
|
const tools = {};
|
|
3481
3484
|
let healthyCount = 0;
|
|
3482
3485
|
// Get all tool names from toolRegistry
|
|
3483
|
-
const allTools = await toolRegistry.listTools();
|
|
3486
|
+
const allTools = await this.toolRegistry.listTools();
|
|
3484
3487
|
const allToolNames = new Set(allTools.map((tool) => tool.name));
|
|
3485
3488
|
for (const toolName of allToolNames) {
|
|
3486
3489
|
const metrics = this.toolExecutionMetrics.get(toolName);
|
|
@@ -3908,7 +3911,7 @@ export class NeuroLink {
|
|
|
3908
3911
|
try {
|
|
3909
3912
|
const externalTools = this.externalServerManager.getServerTools(serverId);
|
|
3910
3913
|
for (const tool of externalTools) {
|
|
3911
|
-
toolRegistry.removeTool(tool.name);
|
|
3914
|
+
this.toolRegistry.removeTool(tool.name);
|
|
3912
3915
|
mcpLogger.debug(`[NeuroLink] Unregistered external MCP tool from main registry: ${tool.name}`);
|
|
3913
3916
|
}
|
|
3914
3917
|
}
|
|
@@ -3921,7 +3924,7 @@ export class NeuroLink {
|
|
|
3921
3924
|
*/
|
|
3922
3925
|
unregisterExternalMCPToolFromRegistry(toolName) {
|
|
3923
3926
|
try {
|
|
3924
|
-
toolRegistry.removeTool(toolName);
|
|
3927
|
+
this.toolRegistry.removeTool(toolName);
|
|
3925
3928
|
mcpLogger.debug(`[NeuroLink] Unregistered external MCP tool from main registry: ${toolName}`);
|
|
3926
3929
|
}
|
|
3927
3930
|
catch (error) {
|
|
@@ -3979,7 +3982,7 @@ export class NeuroLink {
|
|
|
3979
3982
|
try {
|
|
3980
3983
|
const externalTools = this.externalServerManager.getAllTools();
|
|
3981
3984
|
for (const tool of externalTools) {
|
|
3982
|
-
toolRegistry.removeTool(tool.name);
|
|
3985
|
+
this.toolRegistry.removeTool(tool.name);
|
|
3983
3986
|
}
|
|
3984
3987
|
mcpLogger.debug(`[NeuroLink] Unregistered ${externalTools.length} external MCP tools from main registry`);
|
|
3985
3988
|
}
|