import { z } from 'zod/v4' import type { ValidationResult } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { getCwd } from '../../utils/cwd.js' import { isENOENT } from '../../utils/errors.js' import { FILE_NOT_FOUND_CWD_NOTE, suggestPathUnderCwd, } from '../../utils/file.js' import { getFsImplementation } from '../../utils/fsOperations.js' import { glob } from '../../utils/glob.js' import { lazySchema } from '../../utils/lazySchema.js' import { expandPath, toRelativePath } from '../../utils/path.js' import { checkReadPermissionForTool } from '../../utils/permissions/filesystem.js' import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js' import { matchWildcardPattern } from '../../utils/permissions/shellRuleMatching.js' import { DESCRIPTION, GLOB_TOOL_NAME } from './prompt.js' import { getToolUseSummary, renderToolResultMessage, renderToolUseErrorMessage, renderToolUseMessage, userFacingName, } from './UI.js' const inputSchema = lazySchema(() => z.strictObject({ pattern: z.string().describe('The glob pattern to match files against'), path: z .string() .optional() .describe( 'The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.', ), }), ) type InputSchema = ReturnType const outputSchema = lazySchema(() => z.object({ durationMs: z .number() .describe('Time taken to execute the search in milliseconds'), numFiles: z.number().describe('Total number of files found'), filenames: z .array(z.string()) .describe('Array of file paths that match the pattern'), truncated: z .boolean() .describe('Whether results were truncated (limited to 100 files)'), }), ) type OutputSchema = ReturnType export type Output = z.infer export const GlobTool = buildTool({ name: GLOB_TOOL_NAME, searchHint: 'find files by name pattern or wildcard', maxResultSizeChars: 100_000, async description() { return DESCRIPTION }, userFacingName, getToolUseSummary, getActivityDescription(input) { const summary = getToolUseSummary(input) return summary ? `Finding ${summary}` : 'Finding files' }, get inputSchema(): InputSchema { return inputSchema() }, get outputSchema(): OutputSchema { return outputSchema() }, isConcurrencySafe() { return true }, isReadOnly() { return true }, toAutoClassifierInput(input) { return input.pattern }, isSearchOrReadCommand() { return { isSearch: true, isRead: false } }, getPath({ path }): string { return path ? expandPath(path) : getCwd() }, async preparePermissionMatcher({ pattern }) { return rulePattern => matchWildcardPattern(rulePattern, pattern) }, async validateInput({ path }): Promise { // If path is provided, validate that it exists and is a directory if (path) { const fs = getFsImplementation() const absolutePath = expandPath(path) // SECURITY: Skip filesystem operations for UNC paths to prevent NTLM credential leaks. if (absolutePath.startsWith('\\\\') || absolutePath.startsWith('//')) { return { result: true } } let stats try { stats = await fs.stat(absolutePath) } catch (e: unknown) { if (isENOENT(e)) { const cwdSuggestion = await suggestPathUnderCwd(absolutePath) let message = `Directory does not exist: ${path}. ${FILE_NOT_FOUND_CWD_NOTE} ${getCwd()}.` if (cwdSuggestion) { message += ` Did you mean ${cwdSuggestion}?` } return { result: false, message, errorCode: 1, } } throw e } if (!stats.isDirectory()) { return { result: false, message: `Path is not a directory: ${path}`, errorCode: 2, } } } return { result: true } }, async checkPermissions(input, context): Promise { const appState = context.getAppState() return checkReadPermissionForTool( GlobTool, input, appState.toolPermissionContext, ) }, async prompt() { return DESCRIPTION }, renderToolUseMessage, renderToolUseErrorMessage, renderToolResultMessage, // Reuses Grep's render (UI.tsx:65) — shows filenames.join. durationMs/ // numFiles are "Found 3 files in 12ms" chrome (under-count, fine). extractSearchText({ filenames }) { return filenames.join('\n') }, async call(input, { abortController, getAppState, globLimits }) { const start = Date.now() const appState = getAppState() const limit = globLimits?.maxResults ?? 100 const { files, truncated } = await glob( input.pattern, GlobTool.getPath(input), { limit, offset: 0 }, abortController.signal, appState.toolPermissionContext, ) // Relativize paths under cwd to save tokens (same as GrepTool) const filenames = files.map(toRelativePath) const output: Output = { filenames, durationMs: Date.now() - start, numFiles: filenames.length, truncated, } return { data: output, } }, mapToolResultToToolResultBlockParam(output, toolUseID) { if (output.filenames.length === 0) { return { tool_use_id: toolUseID, type: 'tool_result', content: 'No files found', } } return { tool_use_id: toolUseID, type: 'tool_result', content: [ ...output.filenames, ...(output.truncated ? [ '(Results are truncated. Consider using a more specific path or pattern.)', ] : []), ].join('\n'), } }, } satisfies ToolDef)