@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
841 lines • 37.3 kB
JavaScript
/**
* 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 { execFile } from "child_process";
import { logger } from "../utils/logger.js";
import { VertexAI } from "@google-cloud/vertexai";
import { CSVProcessor } from "../utils/csvProcessor.js";
import { shouldEnableBashTool } from "../utils/toolUtils.js";
const MAX_OUTPUT_BYTES = 102400; // 100KB
function truncateOutput(output) {
if (output.length > MAX_OUTPUT_BYTES) {
return (output.slice(0, MAX_OUTPUT_BYTES) + "\n... [output truncated at 100KB]");
}
return output;
}
// 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",
inputSchema: z.object({
timezone: z
.string()
.optional()
.describe('Timezone (e.g., "America/New_York", "Asia/Kolkata"). Defaults to system 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",
inputSchema: 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",
inputSchema: z.object({
path: z
.string()
.describe("Directory path to list (relative or absolute)"),
includeHidden: z
.boolean()
.optional()
.default(false)
.describe("Include hidden files (starting with .)"),
}),
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",
inputSchema: 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)",
inputSchema: 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,
};
}
},
}),
// NOTE: searchFiles was removed to avoid naming conflict with external MCP 'search_files' tool
// from @modelcontextprotocol/server-filesystem which provides the same functionality
// with parameters {path, pattern, excludePatterns}
analyzeCSV: tool({
description: "Analyze CSV file for accurate counting, aggregation, and statistical analysis. Use this for precise data operations like counting rows by column, calculating sums/averages, finding min/max values, etc. The tool reads the file directly - do NOT pass CSV content.",
inputSchema: z.object({
filePath: z
.string()
.refine((inputPath) => {
const resolvedPath = path.resolve(inputPath);
const normalizedPath = resolvedPath
.toLowerCase()
.replace(/\\/g, "/");
const sensitivePatterns = [
"/etc/",
"/sys/",
"/proc/",
"/dev/",
"/root/",
"/.ssh/",
"/private/etc/",
"/private/var/",
"c:/windows/",
"c:/program files/",
"c:/programdata/",
];
return !sensitivePatterns.some((pattern) => normalizedPath.startsWith(pattern));
}, {
message: "Invalid file path: access to system directories is not allowed",
})
.describe("Path to the CSV file to analyze (e.g., 'test/data.csv' or '/absolute/path/file.csv')"),
operation: z
.enum([
"count_by_column",
"sum_by_column",
"average_by_column",
"min_max_by_column",
"describe",
])
.describe("Type of analysis to perform"),
column: z
.string()
.optional()
.default("")
.describe("Column name for the operation (required for most operations)"),
maxRows: z
.number()
.optional()
.default(1000)
.describe("Maximum rows to process (default: 1000)"),
}),
execute: async ({ filePath, operation, column, maxRows = 1000 }) => {
const startTime = Date.now();
logger.info(`[analyzeCSV] 🚀 START: file=${filePath}, operation=${operation}, column=${column}, maxRows=${maxRows}`);
try {
// Resolve file path
logger.debug(`[analyzeCSV] Resolving file: ${filePath}`);
const path = await import("path");
// Resolve path (support both relative and absolute)
const resolvedPath = path.isAbsolute(filePath)
? filePath
: path.resolve(process.cwd(), filePath);
logger.debug(`[analyzeCSV] Resolved path: ${resolvedPath}`);
// Parse CSV using streaming from disk (memory efficient)
logger.info(`[analyzeCSV] Starting CSV parsing (max ${maxRows} rows)...`);
const rows = (await CSVProcessor.parseCSVFile(resolvedPath, maxRows));
logger.info(`[analyzeCSV] ✅ CSV parsing complete: ${rows.length} rows`);
if (rows.length === 0) {
logger.warn(`[analyzeCSV] No data rows found`);
return {
success: false,
error: "No data rows found in CSV",
};
}
// Log column names
const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
logger.info(`[analyzeCSV] Found ${rows.length} rows with columns:`, columnNames);
logger.info(`[analyzeCSV] Executing operation: ${operation}`);
let result;
switch (operation) {
case "count_by_column": {
logger.info(`[analyzeCSV] count_by_column: column=${column}`);
if (!column) {
return {
success: false,
error: "Column name required for count_by_column operation",
};
}
// Count occurrences of each value in the column
const counts = {};
logger.debug(`[analyzeCSV] Counting rows...`);
for (const row of rows) {
const value = row[column];
if (value !== undefined) {
counts[value] = (counts[value] || 0) + 1;
}
}
logger.debug(`[analyzeCSV] Found ${Object.keys(counts).length} unique values`);
// Sort by count descending
logger.debug(`[analyzeCSV] Sorting results...`);
result = Object.fromEntries(Object.entries(counts).sort(([, a], [, b]) => b - a));
logger.info(`[analyzeCSV] ✅ count_by_column complete. Result:`, result);
break;
}
case "sum_by_column": {
logger.info(`[analyzeCSV] sum_by_column: column=${column}`);
if (!column) {
return {
success: false,
error: "Column name required for sum_by_column operation",
};
}
// Sum numeric values from the target column itself for each group
const groups = {};
logger.debug(`[analyzeCSV] Grouping and summing ${rows.length} rows...`);
let processedRows = 0;
let totalNumericValuesFound = 0;
for (const row of rows) {
const key = row[column];
if (!key) {
continue;
}
// Parse numeric value from the target column
const value = row[column];
if (value === undefined || value === null || value === "") {
continue;
}
const num = parseFloat(value);
if (isNaN(num)) {
continue;
}
if (!groups[key]) {
groups[key] = 0;
}
groups[key] += num;
totalNumericValuesFound++;
processedRows++;
if (processedRows % 10 === 0) {
logger.debug(`[analyzeCSV] Processed ${processedRows}/${rows.length} rows`);
}
}
// Fail fast if no numeric data found in the requested column
if (totalNumericValuesFound === 0) {
return {
success: false,
error: `No numeric data found in column "${column}" for sum_by_column operation`,
};
}
logger.debug(`[analyzeCSV] Calculated sums for ${Object.keys(groups).length} groups (${totalNumericValuesFound} numeric values)`);
result = groups;
logger.info(`[analyzeCSV] ✅ sum_by_column complete`);
break;
}
case "average_by_column": {
logger.info(`[analyzeCSV] average_by_column: column=${column}`);
if (!column) {
return {
success: false,
error: "Column name required for average_by_column operation",
};
}
// Average numeric values from the target column itself for each group
const groups = {};
logger.debug(`[analyzeCSV] Grouping and averaging ${rows.length} rows...`);
let processedRows = 0;
let totalNumericValuesFound = 0;
for (const row of rows) {
const key = row[column];
if (!key) {
continue;
}
// Parse numeric value from the target column
const value = row[column];
if (value === undefined || value === null || value === "") {
continue;
}
const num = parseFloat(value);
if (isNaN(num)) {
continue;
}
if (!groups[key]) {
groups[key] = { sum: 0, count: 0 };
}
groups[key].sum += num;
groups[key].count++;
totalNumericValuesFound++;
processedRows++;
if (processedRows % 10 === 0) {
logger.debug(`[analyzeCSV] Processed ${processedRows}/${rows.length} rows`);
}
}
// Fail fast if no numeric data found in the requested column
if (totalNumericValuesFound === 0) {
return {
success: false,
error: `No numeric data found in column "${column}" for average_by_column operation`,
};
}
logger.debug(`[analyzeCSV] Calculated averages for ${Object.keys(groups).length} groups (${totalNumericValuesFound} numeric values)`);
result = Object.fromEntries(Object.entries(groups).map(([k, v]) => [
k,
v.count > 0 ? v.sum / v.count : 0,
]));
logger.info(`[analyzeCSV] ✅ average_by_column complete`);
break;
}
case "min_max_by_column": {
if (!column) {
return {
success: false,
error: "Column name required for min_max_by_column operation",
};
}
const values = rows
.map((row) => row[column])
.filter((v) => v !== undefined && v !== "");
const numericValues = values
.map((v) => parseFloat(v))
.filter((n) => !isNaN(n));
if (numericValues.length === 0) {
return {
success: false,
error: `No numeric data found in column "${column}" for min_max_by_column operation`,
};
}
result = {
min: Math.min(...numericValues),
max: Math.max(...numericValues),
numericCount: numericValues.length,
totalCount: values.length,
};
break;
}
case "describe": {
const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
result = {
total_rows: rows.length,
columns: columnNames,
column_count: columnNames.length,
};
break;
}
default:
return {
success: false,
error: `Unknown operation: ${operation}`,
};
}
const duration = Date.now() - startTime;
logger.info(`[analyzeCSV] 🏁 COMPLETE: ${operation} took ${duration}ms`);
const response = {
success: true,
operation,
column,
result: JSON.stringify(result, null, 2),
rowCount: rows.length,
};
logger.debug(`[analyzeCSV] 📤 RETURNING TO LLM:`, JSON.stringify(response, null, 2));
return response;
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
operation,
column,
};
}
},
}),
// NOTE: executeBashCommand was moved to a separate opt-in export (bashTool) for security.
// It is only included in directAgentTools when NEUROLINK_ENABLE_BASH_TOOL=true or
// toolConfig.enableBashTool is explicitly set to true. See shouldEnableBashTool() in toolUtils.ts.
websearchGrounding: tool({
description: "Search the web for current information using Google Search grounding. Returns raw search data for AI processing.",
inputSchema: 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",
};
}
},
}),
};
/**
* Bash command execution tool - exported separately for opt-in use.
*
* SECURITY: This tool is NOT included in directAgentTools by default.
* It must be explicitly enabled via:
* - Environment variable: NEUROLINK_ENABLE_BASH_TOOL=true
* - Config: toolConfig.enableBashTool = true
*
* Import this directly when you need bash execution capabilities:
* import { bashTool } from '../agent/directTools.js';
*/
export const bashTool = tool({
description: "Execute a bash/shell command and return stdout, stderr, and exit code. Supports full shell syntax including pipes, redirects, and variable expansion. Requires HITL confirmation when enabled.",
inputSchema: z.object({
command: z
.string()
.describe("The shell command to execute (supports pipes, redirects, etc.)"),
timeout: z
.number()
.optional()
.default(30000)
.describe("Timeout in milliseconds (default: 30000, max: 120000)"),
cwd: z
.string()
.optional()
.describe("Working directory (defaults to process.cwd())"),
}),
execute: async ({ command, timeout = 30000, cwd }) => {
try {
const effectiveTimeout = Math.min(Math.max(timeout, 100), 120000);
const resolvedCwd = cwd ? path.resolve(cwd) : process.cwd();
const currentCwd = process.cwd();
// Verify cwd exists before resolving symlinks
if (!fs.existsSync(resolvedCwd) ||
!fs.statSync(resolvedCwd).isDirectory()) {
return {
success: false,
code: -1,
stdout: "",
stderr: "",
error: `Directory does not exist: ${resolvedCwd}`,
};
}
// Security: resolve symlinks and prevent execution outside current directory
try {
const realCwd = fs.realpathSync(currentCwd);
const realResolvedCwd = fs.realpathSync(resolvedCwd);
if (!realResolvedCwd.startsWith(realCwd)) {
return {
success: false,
code: -1,
stdout: "",
stderr: "",
error: "Access denied: Cannot execute commands outside current directory",
};
}
}
catch {
return {
success: false,
code: -1,
stdout: "",
stderr: "",
error: "Access denied: Cannot resolve directory path",
};
}
// Use /bin/bash -c to support full shell syntax (pipes, redirects, etc.)
return await new Promise((resolve) => {
execFile("/bin/bash", ["-c", command], {
timeout: effectiveTimeout,
cwd: resolvedCwd,
maxBuffer: MAX_OUTPUT_BYTES,
}, (error, stdout, stderr) => {
if (error) {
const exitCode = typeof error.code === "number" ? error.code : 1;
resolve({
success: false,
code: exitCode,
stdout: truncateOutput(stdout || ""),
stderr: truncateOutput(stderr || error.message),
error: error.killed ? "Command timed out" : error.message,
});
}
else {
resolve({
success: true,
code: 0,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
});
}
});
});
}
catch (error) {
return {
success: false,
code: -1,
stdout: "",
stderr: "",
error: error instanceof Error ? error.message : String(error),
};
}
},
});
// Conditionally inject executeBashCommand into directAgentTools when opted in.
// This ensures the tool is only available to SDK consumers who explicitly enable it.
if (shouldEnableBashTool()) {
directAgentTools.executeBashCommand = bashTool;
}
// 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,
};
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;
}
const toolRecord = tool;
if (!toolRecord.parameters && !toolRecord.inputSchema) {
logger.error(`Tool ${name} missing parameters/inputSchema`);
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;
}
}
//# sourceMappingURL=directTools.js.map