| 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<typeof inputSchema> |
|
|
| 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<typeof outputSchema> |
|
|
| export type Output = z.infer<OutputSchema> |
|
|
| 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<ValidationResult> { |
| |
| if (path) { |
| const fs = getFsImplementation() |
| const absolutePath = expandPath(path) |
|
|
| |
| 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<PermissionDecision> { |
| const appState = context.getAppState() |
| return checkReadPermissionForTool( |
| GlobTool, |
| input, |
| appState.toolPermissionContext, |
| ) |
| }, |
| async prompt() { |
| return DESCRIPTION |
| }, |
| renderToolUseMessage, |
| renderToolUseErrorMessage, |
| renderToolResultMessage, |
| |
| |
| 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, |
| ) |
| |
| 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<InputSchema, Output>) |
|
|