@@ -17,7 +17,7 @@ import type { CustomToolDefinition, SerializedCustomToolDefinition, CustomToolPa
1717
1818import type { StoredCustomTool , LoadResult } from "./types.js"
1919import { serializeCustomTool } from "./serialize.js"
20- import { runEsbuild } from "./esbuild-runner.js"
20+ import { runEsbuild , NODE_BUILTIN_MODULES , COMMONJS_REQUIRE_BANNER } from "./esbuild-runner.js"
2121
2222export interface RegistryOptions {
2323 /** Directory for caching compiled TypeScript files. */
@@ -236,16 +236,22 @@ export class CustomToolRegistry {
236236
237237 /**
238238 * Clear the TypeScript compilation cache (both in-memory and on disk).
239+ * This removes all tool-specific subdirectories and their contents.
239240 */
240241 clearCache ( ) : void {
241242 this . tsCache . clear ( )
242243
243244 if ( fs . existsSync ( this . cacheDir ) ) {
244245 try {
245- const files = fs . readdirSync ( this . cacheDir )
246- for ( const file of files ) {
247- if ( file . endsWith ( ".mjs" ) ) {
248- fs . unlinkSync ( path . join ( this . cacheDir , file ) )
246+ const entries = fs . readdirSync ( this . cacheDir , { withFileTypes : true } )
247+ for ( const entry of entries ) {
248+ const entryPath = path . join ( this . cacheDir , entry . name )
249+ if ( entry . isDirectory ( ) ) {
250+ // Remove tool-specific subdirectory and all its contents.
251+ fs . rmSync ( entryPath , { recursive : true , force : true } )
252+ } else if ( entry . name . endsWith ( ".mjs" ) ) {
253+ // Also clean up any legacy flat .mjs files from older cache format.
254+ fs . unlinkSync ( entryPath )
249255 }
250256 }
251257 } catch ( error ) {
@@ -259,6 +265,11 @@ export class CustomToolRegistry {
259265 /**
260266 * Dynamically import a TypeScript or JavaScript file.
261267 * TypeScript files are transpiled on-the-fly using esbuild.
268+ *
269+ * For TypeScript files, esbuild bundles the code with these considerations:
270+ * - Node.js built-in modules (fs, path, etc.) are kept external
271+ * - npm packages are bundled with a CommonJS shim for require() compatibility
272+ * - The tool's local node_modules is included in the resolution path
262273 */
263274 private async import ( filePath : string ) : Promise < Record < string , CustomToolDefinition > > {
264275 const absolutePath = path . resolve ( filePath )
@@ -277,19 +288,31 @@ export class CustomToolRegistry {
277288 return import ( `file://${ cachedPath } ` )
278289 }
279290
280- // Ensure cache directory exists.
281- fs . mkdirSync ( this . cacheDir , { recursive : true } )
282-
283291 const hash = createHash ( "sha256" ) . update ( cacheKey ) . digest ( "hex" ) . slice ( 0 , 16 )
284- const tempFile = path . join ( this . cacheDir , `${ hash } .mjs` )
292+
293+ // Use a tool-specific subdirectory to avoid .env file conflicts between tools.
294+ const toolCacheDir = path . join ( this . cacheDir , hash )
295+ fs . mkdirSync ( toolCacheDir , { recursive : true } )
296+
297+ const tempFile = path . join ( toolCacheDir , "bundle.mjs" )
285298
286299 // Check if we have a cached version on disk (from a previous run/instance).
287300 if ( fs . existsSync ( tempFile ) ) {
288301 this . tsCache . set ( cacheKey , tempFile )
289302 return import ( `file://${ tempFile } ` )
290303 }
291304
305+ // Get the tool's directory to include its node_modules in resolution path.
306+ const toolDir = path . dirname ( absolutePath )
307+ const toolNodeModules = path . join ( toolDir , "node_modules" )
308+
309+ // Combine default nodePaths with tool-specific node_modules.
310+ // Tool's node_modules takes priority (listed first).
311+ const nodePaths = fs . existsSync ( toolNodeModules ) ? [ toolNodeModules , ...this . nodePaths ] : this . nodePaths
312+
292313 // Bundle the TypeScript file with dependencies using esbuild CLI.
314+ // - Node.js built-ins are external (they can't be bundled and are always available)
315+ // - npm packages are bundled with CommonJS require() shim for compatibility
293316 await runEsbuild (
294317 {
295318 entryPoint : absolutePath ,
@@ -300,15 +323,54 @@ export class CustomToolRegistry {
300323 bundle : true ,
301324 sourcemap : "inline" ,
302325 packages : "bundle" ,
303- nodePaths : this . nodePaths ,
326+ nodePaths,
327+ external : NODE_BUILTIN_MODULES ,
328+ banner : COMMONJS_REQUIRE_BANNER ,
304329 } ,
305330 this . extensionPath ,
306331 )
307332
333+ // Copy .env files from the tool's source directory to the tool-specific cache directory.
334+ // This allows tools that use dotenv with __dirname to find their .env files,
335+ // while ensuring different tools' .env files don't overwrite each other.
336+ this . copyEnvFiles ( toolDir , toolCacheDir )
337+
308338 this . tsCache . set ( cacheKey , tempFile )
309339 return import ( `file://${ tempFile } ` )
310340 }
311341
342+ /**
343+ * Copy .env files from the tool's source directory to the tool-specific cache directory.
344+ * This allows tools that use dotenv with __dirname to find their .env files,
345+ * while ensuring different tools' .env files don't overwrite each other.
346+ *
347+ * @param toolDir - The directory containing the tool source files
348+ * @param destDir - The tool-specific cache directory to copy .env files to
349+ */
350+ private copyEnvFiles ( toolDir : string , destDir : string ) : void {
351+ try {
352+ const files = fs . readdirSync ( toolDir )
353+ const envFiles = files . filter ( ( f ) => f === ".env" || f . startsWith ( ".env." ) )
354+
355+ for ( const envFile of envFiles ) {
356+ const srcPath = path . join ( toolDir , envFile )
357+ const destPath = path . join ( destDir , envFile )
358+
359+ // Only copy if source is a file (not a directory).
360+ const stat = fs . statSync ( srcPath )
361+ if ( stat . isFile ( ) ) {
362+ fs . copyFileSync ( srcPath , destPath )
363+ console . log ( `[CustomToolRegistry] copied ${ envFile } to tool cache directory` )
364+ }
365+ }
366+ } catch ( error ) {
367+ // Non-fatal: log but don't fail if we can't copy env files.
368+ console . warn (
369+ `[CustomToolRegistry] failed to copy .env files: ${ error instanceof Error ? error . message : String ( error ) } ` ,
370+ )
371+ }
372+ }
373+
312374 /**
313375 * Check if a value is a Zod schema by looking for the _def property
314376 * which is present on all Zod types.
0 commit comments