UNPKG

@juspay/neurolink

Version:

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

521 lines (520 loc) 21.2 kB
/** * Direct Tool Definitions for NeuroLink CLI Agent * Simple, reliable tools that work immediately with Vercel AI SDK */ import { tool } from "ai"; import { z } from "zod"; import * as fs from "fs"; import * as path from "path"; import { logger } from "../utils/logger.js"; import { VertexAI } from "@google-cloud/vertexai"; // Runtime Google Search tool creation - bypasses TypeScript strict typing function createGoogleSearchTools() { const searchTool = {}; // Dynamically assign google_search property at runtime Object.defineProperty(searchTool, "google_search", { value: {}, enumerable: true, configurable: true, }); return [searchTool]; } /** * Direct tool definitions that work immediately with Gemini/AI SDK * These bypass MCP complexity and provide reliable agent functionality */ export const directAgentTools = { getCurrentTime: tool({ description: "Get the current date and time", parameters: z.object({ timezone: z .string() .optional() .describe('Timezone (e.g., "America/New_York", "Asia/Kolkata"). Defaults to local time.'), }), execute: async ({ timezone }) => { try { const now = new Date(); if (timezone) { return { success: true, time: now.toLocaleString("en-US", { timeZone: timezone }), timezone: timezone, iso: now.toISOString(), }; } return { success: true, time: now.toLocaleString(), iso: now.toISOString(), timestamp: now.getTime(), }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } }, }), readFile: tool({ description: "Read the contents of a file from the filesystem", parameters: z.object({ path: z.string().describe("File path to read (relative or absolute)"), }), execute: async ({ path: filePath }) => { try { // Security check - prevent reading outside current directory for relative paths const resolvedPath = path.resolve(filePath); const cwd = process.cwd(); if (!resolvedPath.startsWith(cwd) && !path.isAbsolute(filePath)) { return { success: false, error: `Access denied: Cannot read files outside current directory`, }; } const content = fs.readFileSync(resolvedPath, "utf-8"); const stats = fs.statSync(resolvedPath); return { success: true, content, size: stats.size, path: resolvedPath, lastModified: stats.mtime.toISOString(), }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), path: filePath, }; } }, }), listDirectory: tool({ description: "List files and directories in a specified directory", parameters: z.object({ path: z .string() .describe("Directory path to list (relative or absolute)"), includeHidden: z .boolean() .optional() .describe("Include hidden files (starting with .)") .default(false), }), execute: async ({ path: dirPath, includeHidden }) => { try { const resolvedPath = path.resolve(dirPath); const items = fs.readdirSync(resolvedPath); const filteredItems = includeHidden ? items : items.filter((item) => !item.startsWith(".")); const itemDetails = filteredItems.map((item) => { const itemPath = path.join(resolvedPath, item); const stats = fs.statSync(itemPath); return { name: item, type: stats.isDirectory() ? "directory" : "file", size: stats.isFile() ? stats.size : undefined, lastModified: stats.mtime.toISOString(), }; }); return { success: true, path: resolvedPath, items: itemDetails, count: itemDetails.length, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), path: dirPath, }; } }, }), calculateMath: tool({ description: "Perform mathematical calculations safely", parameters: z.object({ expression: z .string() .describe('Mathematical expression to evaluate (e.g., "2 + 2", "Math.sqrt(16)")'), precision: z .number() .optional() .describe("Number of decimal places for result") .default(2), }), execute: async ({ expression, precision }) => { try { // Simple safe evaluation - only allow basic math operations const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, ""); if (sanitizedExpression !== expression) { // Try Math functions for more complex operations const allowedMathFunctions = [ "Math.abs", "Math.ceil", "Math.floor", "Math.round", "Math.sqrt", "Math.pow", "Math.sin", "Math.cos", "Math.tan", "Math.log", "Math.exp", "Math.PI", "Math.E", ]; let safeExpression = expression; for (const func of allowedMathFunctions) { safeExpression = safeExpression.replace(new RegExp(func, "g"), func); } // Remove remaining non-safe characters except Math functions const mathSafe = /^[0-9+\-*/().\s]|Math\.(abs|ceil|floor|round|sqrt|pow|sin|cos|tan|log|exp|PI|E)/g; if (!safeExpression .split("") .every((char) => mathSafe.test(char) || char === "(" || char === ")" || char === "," || char === " ")) { return { success: false, error: `Unsafe expression: Only basic math operations and Math functions are allowed`, }; } } // Use Function constructor for safe evaluation const result = new Function(`'use strict'; return (${expression})`)(); const roundedResult = typeof result === "number" ? Number(result.toFixed(precision)) : result; return { success: true, expression, result: roundedResult, type: typeof result, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), expression, }; } }, }), writeFile: tool({ description: "Write content to a file (use with caution)", parameters: z.object({ path: z.string().describe("File path to write to"), content: z.string().describe("Content to write to the file"), mode: z .enum(["create", "overwrite", "append"]) .default("create") .describe("Write mode"), }), execute: async ({ path: filePath, content, mode }) => { try { const resolvedPath = path.resolve(filePath); const cwd = process.cwd(); // Security check if (!resolvedPath.startsWith(cwd) && !path.isAbsolute(filePath)) { return { success: false, error: `Access denied: Cannot write files outside current directory`, }; } // Check if file exists for create mode if (mode === "create" && fs.existsSync(resolvedPath)) { return { success: false, error: `File already exists. Use 'overwrite' or 'append' mode to modify existing files.`, }; } let finalContent = content; if (mode === "append" && fs.existsSync(resolvedPath)) { const existingContent = fs.readFileSync(resolvedPath, "utf-8"); finalContent = existingContent + content; } fs.writeFileSync(resolvedPath, finalContent, "utf-8"); const stats = fs.statSync(resolvedPath); return { success: true, path: resolvedPath, mode, size: stats.size, written: content.length, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), path: filePath, }; } }, }), searchFiles: tool({ description: "Search for files by name pattern in a directory", parameters: z.object({ directory: z.string().describe("Directory to search in"), pattern: z .string() .describe("File name pattern to search for (supports wildcards like *.js)"), recursive: z .boolean() .optional() .default(true) .describe("Search recursively in subdirectories"), }), execute: async ({ directory, pattern, recursive }) => { try { const resolvedDir = path.resolve(directory); if (!fs.existsSync(resolvedDir)) { return { success: false, error: `Directory does not exist: ${resolvedDir}`, }; } const matches = []; const searchDir = (dir, depth = 0) => { if (!recursive && depth > 0) { return; } const items = fs.readdirSync(dir); for (const item of items) { const itemPath = path.join(dir, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { if (recursive && depth < 10) { // Prevent infinite recursion searchDir(itemPath, depth + 1); } } else if (stats.isFile()) { // Simple pattern matching (convert * to regex) const regexPattern = pattern .replace(/\*/g, ".*") .replace(/\?/g, "."); const regex = new RegExp(`^${regexPattern}$`, "i"); if (regex.test(item)) { matches.push({ name: item, path: itemPath, size: stats.size, lastModified: stats.mtime.toISOString(), }); } } } }; searchDir(resolvedDir); return { success: true, directory: resolvedDir, pattern, matches, count: matches.length, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), directory, pattern, }; } }, }), websearchGrounding: tool({ description: "Search the web for current information using Google Search grounding. Returns raw search data for AI processing.", parameters: z.object({ query: z.string().describe("Search query to find information about"), maxResults: z .number() .optional() .default(3) .describe("Maximum number of search results to return (1-5)"), maxWords: z .number() .optional() .default(50) .describe("Maximum number of words in the response 50"), }), execute: async ({ query, maxResults = 3, maxWords = 50 }) => { try { const hasCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS; const hasProjectId = process.env.GOOGLE_VERTEX_PROJECT; const projectLocation = process.env.GOOGLE_VERTEX_LOCATION || "us-central1"; if (!hasCredentials || !hasProjectId) { return { success: false, error: "Google Vertex AI credentials not configured. Please set GOOGLE_APPLICATION_CREDENTIALS and GOOGLE_VERTEX_PROJECT environment variables.", requiredEnvVars: [ "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_VERTEX_PROJECT", ], }; } const limitedResults = Math.min(Math.max(maxResults, 1), 5); const vertex_ai = new VertexAI({ project: hasProjectId, location: projectLocation, }); const websearchModel = "gemini-2.5-flash-lite"; const model = vertex_ai.getGenerativeModel({ model: websearchModel, tools: createGoogleSearchTools(), }); // Search query with word limit constraint const searchPrompt = `Search for: "${query}". Provide a concise summary in no more than ${maxWords} words.`; const startTime = Date.now(); const response = await model.generateContent({ contents: [ { role: "user", parts: [{ text: searchPrompt }], }, ], }); const responseTime = Date.now() - startTime; // Extract grounding metadata and search results const result = response.response; const candidates = result.candidates; if (!candidates || candidates.length === 0) { return { success: false, error: "No search results returned", query, }; } const content = candidates[0].content; if (!content || !content.parts || content.parts.length === 0) { return { success: false, error: "No search content found", query, }; } // Extract raw search content const searchContent = content.parts[0].text || ""; // Extract grounding sources if available const groundingMetadata = candidates[0]?.groundingMetadata; const searchResults = []; if (groundingMetadata?.groundingChunks) { for (const chunk of groundingMetadata.groundingChunks.slice(0, limitedResults)) { if (chunk.web) { searchResults.push({ title: chunk.web.title || "No title", url: chunk.web.uri || "", snippet: searchContent, // Use full content since maxWords already limits length domain: chunk.web.uri ? new URL(chunk.web.uri).hostname : "unknown", }); } } } // If no grounding metadata, create basic result structure if (searchResults.length === 0) { searchResults.push({ title: `Search results for: ${query}`, url: "", snippet: searchContent, domain: "google-search", }); } return { success: true, query, searchResults, rawContent: searchContent, totalResults: searchResults.length, provider: "google-search-grounding", model: websearchModel, responseTime, timestamp: startTime, grounded: true, }; } catch (error) { logger.error("Web search grounding error:", error); return { success: false, error: error instanceof Error ? error.message : String(error), query, provider: "google-search-grounding", }; } }, }), }; // eslint-disable-next-line no-redeclare export function getToolsForCategory(category = "all") { switch (category) { case "basic": return { getCurrentTime: directAgentTools.getCurrentTime, calculateMath: directAgentTools.calculateMath, }; case "filesystem": return { readFile: directAgentTools.readFile, listDirectory: directAgentTools.listDirectory, writeFile: directAgentTools.writeFile, searchFiles: directAgentTools.searchFiles, }; case "utility": return { getCurrentTime: directAgentTools.getCurrentTime, calculateMath: directAgentTools.calculateMath, listDirectory: directAgentTools.listDirectory, }; case "all": default: return directAgentTools; } } /** * Get tool names for validation */ export function getAvailableToolNames() { return Object.keys(directAgentTools); } /** * Validate that all tools have proper structure */ export function validateToolStructure() { try { for (const [name, tool] of Object.entries(directAgentTools)) { if (!tool.description || typeof tool.description !== "string") { logger.error(`❌ Tool ${name} missing description`); return false; } if (!tool.parameters) { logger.error(`❌ Tool ${name} missing parameters`); return false; } if (!tool.execute || typeof tool.execute !== "function") { logger.error(`❌ Tool ${name} missing execute function`); return false; } } logger.info("✅ All tools have valid structure"); return true; } catch (error) { logger.error("❌ Tool validation failed:", error); return false; } }