onetool-mcp 1.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. ot_tools/web_fetch.py +384 -0
ot_tools/ripgrep.py ADDED
@@ -0,0 +1,544 @@
1
+ """Ripgrep text search tools.
2
+
3
+ Provides fast text and regex search in files using ripgrep (rg).
4
+ Requires the `rg` binary in PATH (install with: brew install ripgrep).
5
+
6
+ Inspired by mcp-ripgrep (https://github.com/mcollina/mcp-ripgrep)
7
+ by Matteo Collina, licensed under MIT.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ # Pack for dot notation: ripgrep.search(), ripgrep.count(), etc.
13
+ pack = "ripgrep"
14
+
15
+ __all__ = ["count", "files", "search", "types"]
16
+
17
+ # Dependency declarations for CLI validation
18
+ __ot_requires__ = {
19
+ "cli": [("rg", "brew install ripgrep")],
20
+ }
21
+
22
+ import contextlib
23
+ import shutil
24
+ import subprocess
25
+ from typing import TYPE_CHECKING
26
+
27
+ from pydantic import BaseModel, Field
28
+
29
+ if TYPE_CHECKING:
30
+ from pathlib import Path
31
+
32
+ from ot.config import get_tool_config
33
+ from ot.logging import LogSpan
34
+ from ot.paths import resolve_cwd_path
35
+ from ot.utils.platform import get_install_hint
36
+
37
+
38
+ class Config(BaseModel):
39
+ """Pack configuration - discovered by registry."""
40
+
41
+ timeout: float = Field(
42
+ default=60.0,
43
+ ge=1.0,
44
+ le=300.0,
45
+ description="Command timeout in seconds",
46
+ )
47
+ relative_paths: bool = Field(
48
+ default=True,
49
+ description="Output relative paths instead of absolute paths",
50
+ )
51
+
52
+
53
+ def _resolve_path(path: str) -> Path:
54
+ """Resolve a search path relative to project directory.
55
+
56
+ Uses SDK resolve_cwd_path() for consistent path resolution.
57
+
58
+ Path resolution follows project conventions:
59
+ - Relative paths: resolved relative to project directory (OT_CWD)
60
+ - Absolute paths: used as-is
61
+ - ~ paths: expanded to home directory
62
+ - Prefixed paths (CWD/, GLOBAL/, OT_DIR/): resolved to respective dirs
63
+
64
+ Args:
65
+ path: Path string (can contain ~ or prefixes)
66
+
67
+ Returns:
68
+ Resolved absolute Path
69
+ """
70
+ return resolve_cwd_path(path)
71
+
72
+
73
+ def _to_relative_output(output: str, base_path: Path) -> str:
74
+ """Convert absolute paths in ripgrep output to relative paths.
75
+
76
+ Args:
77
+ output: Raw ripgrep output with absolute paths
78
+ base_path: Base path to make paths relative to
79
+
80
+ Returns:
81
+ Output with paths converted to relative
82
+ """
83
+ cfg = get_tool_config("ripgrep", Config)
84
+ if not cfg.relative_paths:
85
+ return output
86
+
87
+ base_str = str(base_path)
88
+ lines = []
89
+ for line in output.split("\n"):
90
+ if line.startswith(base_str):
91
+ # Convert absolute path to relative
92
+ rel_line = line[len(base_str) :].lstrip("/\\")
93
+ lines.append(rel_line)
94
+ else:
95
+ lines.append(line)
96
+ return "\n".join(lines)
97
+
98
+
99
+ def _check_rg_installed() -> str | None:
100
+ """Check if ripgrep is installed.
101
+
102
+ Returns:
103
+ None if installed, error message if not.
104
+ """
105
+ if shutil.which("rg") is None:
106
+ return f"Error: ripgrep (rg) is not installed. {get_install_hint('rg')}"
107
+ return None
108
+
109
+
110
+ def _run_rg(
111
+ args: list[str], cwd: str | None = None, timeout: float | None = None
112
+ ) -> tuple[bool, str]:
113
+ """Run ripgrep with the given arguments.
114
+
115
+ Args:
116
+ args: Command line arguments for rg
117
+ cwd: Working directory for the command
118
+ timeout: Command timeout in seconds (defaults to config)
119
+
120
+ Returns:
121
+ Tuple of (success, output). If success is False, output contains error message.
122
+ """
123
+ if timeout is None:
124
+ timeout = get_tool_config("ripgrep", Config).timeout
125
+
126
+ with LogSpan(span="ripgrep.exec", args=args[:3] if len(args) > 3 else args) as span:
127
+ try:
128
+ result = subprocess.run(
129
+ ["rg", *args],
130
+ capture_output=True,
131
+ text=True,
132
+ cwd=cwd,
133
+ timeout=timeout,
134
+ )
135
+
136
+ # rg returns 1 for no matches (not an error), 2 for actual errors
137
+ if result.returncode == 2:
138
+ error_msg = result.stderr.strip() or "Unknown ripgrep error"
139
+ span.add(returncode=2, error=error_msg)
140
+ # Improve error message for common patterns
141
+ if "regex parse error" in error_msg:
142
+ return False, f"Error: Invalid regex pattern\n{error_msg}"
143
+ return False, f"Error: {error_msg}"
144
+
145
+ span.add(returncode=result.returncode, outputLen=len(result.stdout))
146
+ return True, result.stdout
147
+
148
+ except subprocess.TimeoutExpired:
149
+ span.add(error="timeout")
150
+ return False, f"Error: Search timed out after {timeout} seconds"
151
+ except FileNotFoundError:
152
+ span.add(error="not_installed")
153
+ return (
154
+ False,
155
+ f"Error: ripgrep (rg) is not installed. {get_install_hint('rg')}",
156
+ )
157
+ except Exception as e:
158
+ span.add(error=str(e))
159
+ return False, f"Error: {e}"
160
+
161
+
162
+ def search(
163
+ *,
164
+ pattern: str,
165
+ path: str = ".",
166
+ case_sensitive: bool = True,
167
+ fixed_strings: bool = False,
168
+ file_type: str | None = None,
169
+ glob: str | None = None,
170
+ context: int = 0,
171
+ before_context: int = 0,
172
+ after_context: int = 0,
173
+ max_per_file: int | None = None,
174
+ limit: int | None = None,
175
+ word_match: bool = False,
176
+ include_hidden: bool = False,
177
+ invert_match: bool = False,
178
+ multiline: bool = False,
179
+ only_matching: bool = False,
180
+ no_ignore: bool = False,
181
+ heading: bool = False,
182
+ ) -> str:
183
+ """Search files for patterns using ripgrep.
184
+
185
+ Performs fast text/regex search across files. Returns matching lines
186
+ with file paths and line numbers.
187
+
188
+ Args:
189
+ pattern: The regex or literal pattern to search for
190
+ path: Directory or file to search in (default: current directory)
191
+ case_sensitive: Match case-sensitively (default: True)
192
+ fixed_strings: Treat pattern as literal string, not regex (default: False)
193
+ file_type: Search only files of this type (e.g., "py", "js", "ts")
194
+ glob: Search only files matching this glob pattern. Note: glob is applied
195
+ relative to `path`. Use `glob="**/*.py"` with `path="."` for absolute
196
+ patterns, or `glob="*.py"` with `path="src/"` for relative patterns.
197
+ context: Number of lines to show before and after each match
198
+ before_context: Number of lines to show before each match (overrides context)
199
+ after_context: Number of lines to show after each match (overrides context)
200
+ max_per_file: Maximum matches per file (passed to rg --max-count)
201
+ limit: Maximum total matching lines to return (post-processed)
202
+ word_match: Match whole words only (default: False)
203
+ include_hidden: Search hidden files and directories (default: False)
204
+ invert_match: Return lines NOT matching the pattern (default: False)
205
+ multiline: Match patterns spanning multiple lines (default: False)
206
+ only_matching: Show only the matched text, not the full line (default: False)
207
+ no_ignore: Don't respect .gitignore files (default: False)
208
+ heading: Group matches by file with headings (default: False)
209
+
210
+ Returns:
211
+ Matching lines with file paths and line numbers, or error message.
212
+
213
+ Example:
214
+ # Basic search
215
+ ripgrep.search(pattern="TODO", path="src/")
216
+
217
+ # Case insensitive search in Python files
218
+ ripgrep.search(pattern="error", path=".", case_sensitive=False, file_type="py")
219
+
220
+ # Fixed string search (pattern with special chars)
221
+ ripgrep.search(pattern="[test]", path=".", fixed_strings=True)
222
+
223
+ # Search with context
224
+ ripgrep.search(pattern="def main", path=".", context=3, limit=5)
225
+
226
+ # Glob patterns are relative to path
227
+ ripgrep.search(pattern="TODO", glob="**/*.py", path=".") # All .py files
228
+ ripgrep.search(pattern="TODO", glob="*.py", path="src/") # Only src/*.py
229
+
230
+ # Find lines NOT containing a pattern
231
+ ripgrep.search(pattern="import", path=".", invert_match=True, file_type="py")
232
+
233
+ # Multiline patterns
234
+ ripgrep.search(pattern="def.*\\n.*return", path=".", multiline=True)
235
+ """
236
+ with LogSpan(span="ripgrep.search", pattern=pattern, path=path) as s:
237
+ # Check rg is installed
238
+ error = _check_rg_installed()
239
+ if error:
240
+ s.add("error", "not_installed")
241
+ return error
242
+
243
+ # Resolve path relative to effective cwd
244
+ search_path = _resolve_path(path)
245
+ if not search_path.exists():
246
+ s.add("error", "invalid_path")
247
+ return f"Error: Path does not exist: {search_path}"
248
+
249
+ # Build arguments
250
+ args = ["--line-number", "--with-filename"]
251
+
252
+ if not case_sensitive:
253
+ args.append("--ignore-case")
254
+
255
+ if fixed_strings:
256
+ args.append("--fixed-strings")
257
+
258
+ if file_type:
259
+ args.extend(["--type", file_type])
260
+
261
+ if glob:
262
+ args.extend(["--glob", glob])
263
+
264
+ # Context options: specific before/after take precedence over general context
265
+ if before_context > 0:
266
+ args.extend(["-B", str(before_context)])
267
+ if after_context > 0:
268
+ args.extend(["-A", str(after_context)])
269
+ if context > 0 and before_context == 0 and after_context == 0:
270
+ args.extend(["--context", str(context)])
271
+
272
+ if max_per_file:
273
+ args.extend(["--max-count", str(max_per_file)])
274
+
275
+ if word_match:
276
+ args.append("--word-regexp")
277
+
278
+ if include_hidden:
279
+ args.append("--hidden")
280
+
281
+ if invert_match:
282
+ args.append("--invert-match")
283
+
284
+ if multiline:
285
+ args.append("--multiline")
286
+
287
+ if only_matching:
288
+ args.append("--only-matching")
289
+
290
+ if no_ignore:
291
+ args.append("--no-ignore")
292
+
293
+ if heading:
294
+ args.append("--heading")
295
+
296
+ args.extend([pattern, str(search_path)])
297
+
298
+ success, output = _run_rg(args)
299
+
300
+ if not success:
301
+ s.add("error", output)
302
+ return output
303
+
304
+ if not output.strip():
305
+ s.add("matchCount", 0)
306
+ return "No matches found"
307
+
308
+ # Convert to relative paths if configured
309
+ result = _to_relative_output(output.strip(), search_path)
310
+
311
+ # Apply total limit if specified (post-process)
312
+ lines = result.split("\n")
313
+ if limit and len(lines) > limit:
314
+ result = "\n".join(lines[:limit])
315
+ result += f"\n... ({len(lines) - limit} more matches truncated)"
316
+
317
+ # Count matches
318
+ match_count = min(len(lines), limit) if limit else len(lines)
319
+ s.add("matchCount", match_count)
320
+
321
+ return result
322
+
323
+
324
+ def count(
325
+ *,
326
+ pattern: str,
327
+ path: str = ".",
328
+ count_all: bool = False,
329
+ file_type: str | None = None,
330
+ glob: str | None = None,
331
+ include_hidden: bool = False,
332
+ no_ignore: bool = False,
333
+ ) -> str:
334
+ """Count pattern occurrences in files.
335
+
336
+ Returns file paths with match counts per file.
337
+
338
+ Args:
339
+ pattern: The regex or literal pattern to count
340
+ path: Directory or file to search in (default: current directory)
341
+ count_all: Count all matches per line, not just matching lines (default: False)
342
+ file_type: Count only in files of this type (e.g., "py", "js")
343
+ glob: Count only in files matching this glob pattern. Note: glob is applied
344
+ relative to `path`.
345
+ include_hidden: Include hidden files and directories (default: False)
346
+ no_ignore: Don't respect .gitignore files (default: False)
347
+
348
+ Returns:
349
+ File paths with match counts, or error message.
350
+
351
+ Example:
352
+ # Count TODOs in source files
353
+ ripgrep.count(pattern="TODO", path="src/")
354
+
355
+ # Count all imports (including multiple per line)
356
+ ripgrep.count(pattern="import", path=".", count_all=True, file_type="py")
357
+
358
+ # Count with glob patterns (relative to path)
359
+ ripgrep.count(pattern="TODO", glob="**/*.py", path=".")
360
+ ripgrep.count(pattern="import", glob="*.{js,ts}", path="src/")
361
+ """
362
+ with LogSpan(span="ripgrep.count", pattern=pattern, path=path) as s:
363
+ # Check rg is installed
364
+ error = _check_rg_installed()
365
+ if error:
366
+ s.add("error", "not_installed")
367
+ return error
368
+
369
+ # Resolve path relative to effective cwd
370
+ search_path = _resolve_path(path)
371
+ if not search_path.exists():
372
+ s.add("error", "invalid_path")
373
+ return f"Error: Path does not exist: {search_path}"
374
+
375
+ # Build arguments
376
+ args = ["--count"]
377
+
378
+ if count_all:
379
+ args.append("--count-matches")
380
+
381
+ if file_type:
382
+ args.extend(["--type", file_type])
383
+
384
+ if glob:
385
+ args.extend(["--glob", glob])
386
+
387
+ if include_hidden:
388
+ args.append("--hidden")
389
+
390
+ if no_ignore:
391
+ args.append("--no-ignore")
392
+
393
+ args.extend([pattern, str(search_path)])
394
+
395
+ success, output = _run_rg(args)
396
+
397
+ if not success:
398
+ s.add("error", output)
399
+ return output
400
+
401
+ if not output.strip():
402
+ s.add("matchCount", 0)
403
+ return "No matches found"
404
+
405
+ # Convert to relative paths if configured
406
+ result = _to_relative_output(output.strip(), search_path)
407
+
408
+ # Sum total matches
409
+ total = 0
410
+ for line in result.split("\n"):
411
+ if ":" in line:
412
+ with contextlib.suppress(ValueError):
413
+ total += int(line.split(":")[-1])
414
+
415
+ s.add("totalCount", total)
416
+ s.add("fileCount", len(result.split("\n")))
417
+
418
+ return result
419
+
420
+
421
+ def files(
422
+ *,
423
+ path: str = ".",
424
+ file_type: str | None = None,
425
+ glob: str | None = None,
426
+ include_hidden: bool = False,
427
+ no_ignore: bool = False,
428
+ sort: str | None = None,
429
+ ) -> str:
430
+ """List files that would be searched.
431
+
432
+ Returns a list of file paths that ripgrep would search based on filters.
433
+
434
+ Args:
435
+ path: Directory to list files in (default: current directory)
436
+ file_type: List only files of this type (e.g., "py", "js")
437
+ glob: List only files matching this glob pattern. Note: glob is applied
438
+ relative to `path`.
439
+ include_hidden: Include hidden files and directories (default: False)
440
+ no_ignore: Don't respect .gitignore files (default: False)
441
+ sort: Sort files by: "path", "modified", "accessed", or "created"
442
+
443
+ Returns:
444
+ List of file paths, one per line.
445
+
446
+ Example:
447
+ # List all Python files
448
+ ripgrep.files(path="src/", file_type="py")
449
+
450
+ # List markdown files
451
+ ripgrep.files(path=".", glob="*.md")
452
+
453
+ # Glob patterns are relative to path
454
+ ripgrep.files(glob="**/*.py", path=".") # All .py files from root
455
+ ripgrep.files(glob="test_*.py", path="tests/") # Test files in tests/
456
+
457
+ # List files sorted by modification time
458
+ ripgrep.files(path="src/", file_type="py", sort="modified")
459
+
460
+ # Include files normally ignored by .gitignore
461
+ ripgrep.files(path=".", no_ignore=True)
462
+ """
463
+ with LogSpan(span="ripgrep.files", path=path) as s:
464
+ # Check rg is installed
465
+ error = _check_rg_installed()
466
+ if error:
467
+ s.add("error", "not_installed")
468
+ return error
469
+
470
+ # Resolve path relative to effective cwd
471
+ search_path = _resolve_path(path)
472
+ if not search_path.exists():
473
+ s.add("error", "invalid_path")
474
+ return f"Error: Path does not exist: {search_path}"
475
+
476
+ # Build arguments
477
+ args = ["--files"]
478
+
479
+ if file_type:
480
+ args.extend(["--type", file_type])
481
+
482
+ if glob:
483
+ args.extend(["--glob", glob])
484
+
485
+ if include_hidden:
486
+ args.append("--hidden")
487
+
488
+ if no_ignore:
489
+ args.append("--no-ignore")
490
+
491
+ if sort:
492
+ args.extend(["--sort", sort])
493
+
494
+ args.append(str(search_path))
495
+
496
+ success, output = _run_rg(args)
497
+
498
+ if not success:
499
+ s.add("error", output)
500
+ return output
501
+
502
+ if not output.strip():
503
+ s.add("fileCount", 0)
504
+ return "No files found"
505
+
506
+ # Convert to relative paths if configured
507
+ result = _to_relative_output(output.strip(), search_path)
508
+
509
+ file_count = len(result.split("\n"))
510
+ s.add("fileCount", file_count)
511
+
512
+ return result
513
+
514
+
515
+ def types() -> str:
516
+ """List supported file types.
517
+
518
+ Returns all file types that ripgrep recognizes, with their
519
+ associated file extensions and patterns.
520
+
521
+ Returns:
522
+ List of file types with extensions.
523
+
524
+ Example:
525
+ # Show all supported types
526
+ ripgrep.types()
527
+ """
528
+ with LogSpan(span="ripgrep.types") as s:
529
+ # Check rg is installed
530
+ error = _check_rg_installed()
531
+ if error:
532
+ s.add("error", "not_installed")
533
+ return error
534
+
535
+ success, output = _run_rg(["--type-list"])
536
+
537
+ if not success:
538
+ s.add("error", output)
539
+ return output
540
+
541
+ type_count = len(output.strip().split("\n"))
542
+ s.add("typeCount", type_count)
543
+
544
+ return output.strip()