createGrepTool function

Tool<JsonMap, String> createGrepTool(
  1. Genkit ai, {
  2. required ToolRuntime runtime,
})

Builds the tool that searches file contents with ripgrep.

Implementation

Tool<JsonMap, String> createGrepTool(
  Genkit ai, {
  required ToolRuntime runtime,
}) {
  return defineTextTool(
    ai,
    runtime: runtime,
    name: 'grep',
    description: _description,
    properties: <String, Object?>{
      'pattern': stringProperty('The regular expression to search for.'),
      'path': stringProperty('File or directory to search. Defaults to the current working directory.'),
      'glob': stringProperty('Glob to filter files, e.g. "*.dart" or "src/**/*.{ts,tsx}".'),
      'type': stringProperty('ripgrep file type to search, e.g. "dart", "py", "rust".'),
      'output_mode': stringProperty(
        'What to return. Defaults to "files_with_matches".',
        enumValues: <String>['content', 'files_with_matches', 'count'],
      ),
      '-i': booleanProperty('Case-insensitive search.'),
      '-A': integerProperty('Lines of context after each match (content mode only).'),
      '-B': integerProperty('Lines of context before each match (content mode only).'),
      '-C': integerProperty('Lines of context around each match (content mode only).'),
      'multiline': booleanProperty('Let `.` match newlines and patterns span lines.'),
      'head_limit': integerProperty('Maximum number of output lines. Defaults to $_defaultHeadLimit.'),
    },
    required: <String>['pattern'],
    run: (input) async {
      final pattern = requireString(input, 'pattern');
      final pathInput = readString(input, 'path');
      final glob = readString(input, 'glob')?.trim();
      final type = readString(input, 'type')?.trim();
      final outputMode = readString(input, 'output_mode')?.trim() ?? 'files_with_matches';
      final headLimit = readInt(input, 'head_limit') ?? _defaultHeadLimit;

      final searchPath = pathInput == null ? runtime.currentWorkingDirectory : runtime.resolvePath(pathInput);
      final isContent = outputMode == 'content';
      final args = <String>[
        switch (outputMode) {
          'content' => '--line-number',
          'count' => '--count',
          'files_with_matches' => '--files-with-matches',
          _ => throw const ToolFailure('`output_mode` must be content, files_with_matches, or count.'),
        },
        '--no-heading',
        '--color=never',
        '--max-columns=500',
        if (readBool(input, '-i') ?? false) '--ignore-case',
        if (readBool(input, 'multiline') ?? false) ...<String>['--multiline', '--multiline-dotall'],
        if (glob != null && glob.isNotEmpty) ...<String>['--glob', glob],
        if (type != null && type.isNotEmpty) ...<String>['--type', type],
        if (isContent) ..._contextArgs(input),
        '--',
        pattern,
        searchPath,
      ];

      runtime.startTool('grep', '$pattern in ${runtime.displayPath(searchPath)}');
      final ProcessResult result;
      try {
        result = await Process.run('rg', args, workingDirectory: runtime.currentWorkingDirectory);
      } on ProcessException {
        throw const ToolFailure('ripgrep (`rg`) is not installed. Ask the user to install it.');
      }

      if (result.exitCode == 1) {
        return 'No matches found.';
      }
      if (result.exitCode > 1) {
        throw ToolFailure('rg exited with code ${result.exitCode}: ${'${result.stderr}'.trim()}');
      }

      final lines = '${result.stdout}'.trimRight().split('\n');
      final shown = headLimit > 0 && lines.length > headLimit ? lines.take(headLimit).toList() : lines;
      final output = truncateMiddle(shown.join('\n'), _maxOutputChars);
      if (shown.length < lines.length) {
        return '$output\n\n(Showing $headLimit of ${lines.length} lines. Narrow the search to see more.)';
      }
      return output;
    },
  );
}