( tools: any, vars?: Record<string, VarValue>, )
| 485 | * @throws {Error} If the loaded tools are in an invalid format |
| 486 | */ |
| 487 | export async function maybeLoadToolsFromExternalFile( |
| 488 | tools: any, |
| 489 | vars?: Record<string, VarValue>, |
| 490 | ): Promise<any> { |
| 491 | const rendered = renderVarsInObject(tools, vars); |
| 492 | |
| 493 | // Check if this is a Python/JS file reference with function name |
| 494 | // These need special handling to execute the function and get the result |
| 495 | if (typeof rendered === 'string' && rendered.startsWith('file://')) { |
| 496 | const { filePath, functionName } = parseFileUrl(rendered); |
| 497 | |
| 498 | if (functionName && (filePath.endsWith('.py') || isJavascriptFile(filePath))) { |
| 499 | // Execute the function to get tool definitions |
| 500 | const fileType = filePath.endsWith('.py') ? 'Python' : 'JavaScript'; |
| 501 | logger.debug( |
| 502 | `[maybeLoadToolsFromExternalFile] Loading tools from ${fileType} file: ${filePath}:${functionName}`, |
| 503 | ); |
| 504 | |
| 505 | try { |
| 506 | let toolDefinitions: any; |
| 507 | |
| 508 | if (filePath.endsWith('.py')) { |
| 509 | // Resolve Python path relative to config base directory (same as JavaScript) |
| 510 | const absPath = safeResolve(cliState.basePath || process.cwd(), filePath); |
| 511 | logger.debug(`[maybeLoadToolsFromExternalFile] Resolved Python path: ${absPath}`); |
| 512 | toolDefinitions = await runPython(absPath, functionName, []); |
| 513 | } else { |
| 514 | // Use safeResolve for security (prevents path traversal) |
| 515 | const absPath = safeResolve(cliState.basePath || process.cwd(), filePath); |
| 516 | logger.debug(`[maybeLoadToolsFromExternalFile] Resolved JavaScript path: ${absPath}`); |
| 517 | |
| 518 | const module = await importModule(absPath); |
| 519 | const fn = module[functionName] || module.default?.[functionName]; |
| 520 | |
| 521 | if (typeof fn !== 'function') { |
| 522 | const availableExports = Object.keys(module).filter((k) => k !== 'default'); |
| 523 | const basePath = cliState.basePath || process.cwd(); |
| 524 | throw new Error( |
| 525 | `Function "${functionName}" not found in ${filePath}. ` + |
| 526 | `Available exports: ${availableExports.length > 0 ? availableExports.join(', ') : '(none)'}\n` + |
| 527 | `Resolved from: ${basePath}`, |
| 528 | ); |
| 529 | } |
| 530 | |
| 531 | // Call the function - handle both sync and async functions |
| 532 | toolDefinitions = await Promise.resolve(fn()); |
| 533 | } |
| 534 | |
| 535 | // Validate the result - must be array or object, not primitive |
| 536 | if ( |
| 537 | !toolDefinitions || |
| 538 | typeof toolDefinitions === 'string' || |
| 539 | typeof toolDefinitions === 'number' || |
| 540 | typeof toolDefinitions === 'boolean' |
| 541 | ) { |
| 542 | throw new Error( |
| 543 | `Function "${functionName}" must return an array or object of tool definitions, ` + |
| 544 | `but returned: ${toolDefinitions === null ? 'null' : typeof toolDefinitions}`, |
no test coverage detected
searching dependent graphs…