tunacode-cli 0.0.55__py3-none-any.whl → 0.0.57__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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands/implementations/plan.py +50 -0
- tunacode/cli/commands/registry.py +3 -0
- tunacode/cli/repl.py +327 -186
- tunacode/cli/repl_components/command_parser.py +37 -4
- tunacode/cli/repl_components/error_recovery.py +79 -1
- tunacode/cli/repl_components/output_display.py +21 -1
- tunacode/cli/repl_components/tool_executor.py +12 -0
- tunacode/configuration/defaults.py +8 -0
- tunacode/constants.py +10 -2
- tunacode/core/agents/agent_components/agent_config.py +212 -22
- tunacode/core/agents/agent_components/node_processor.py +46 -40
- tunacode/core/code_index.py +83 -29
- tunacode/core/state.py +44 -0
- tunacode/core/token_usage/usage_tracker.py +2 -2
- tunacode/core/tool_handler.py +20 -0
- tunacode/prompts/system.md +117 -490
- tunacode/services/mcp.py +29 -7
- tunacode/tools/base.py +110 -0
- tunacode/tools/bash.py +96 -1
- tunacode/tools/exit_plan_mode.py +273 -0
- tunacode/tools/glob.py +366 -33
- tunacode/tools/grep.py +226 -77
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/present_plan.py +288 -0
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +99 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/todo.py +108 -1
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/types.py +58 -0
- tunacode/ui/input.py +14 -2
- tunacode/ui/keybindings.py +25 -4
- tunacode/ui/panels.py +53 -8
- tunacode/ui/prompt_manager.py +25 -2
- tunacode/ui/tool_ui.py +3 -2
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/message_utils.py +14 -4
- tunacode/utils/ripgrep.py +332 -9
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/METADATA +8 -3
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/RECORD +46 -42
- tunacode/tools/read_file_async_poc.py +0 -196
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/top_level.txt +0 -0
tunacode/tools/glob.py
CHANGED
|
@@ -8,9 +8,15 @@ complementing the grep tool's content search with fast filename-based searching.
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import fnmatch
|
|
10
10
|
import os
|
|
11
|
+
import re
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from functools import lru_cache
|
|
11
14
|
from pathlib import Path
|
|
12
|
-
from typing import List, Optional
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
13
16
|
|
|
17
|
+
import defusedxml.ElementTree as ET
|
|
18
|
+
|
|
19
|
+
from tunacode.core.code_index import CodeIndex
|
|
14
20
|
from tunacode.exceptions import ToolExecutionError
|
|
15
21
|
from tunacode.tools.base import BaseTool
|
|
16
22
|
|
|
@@ -35,13 +41,160 @@ EXCLUDE_DIRS = {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
|
|
44
|
+
class SortOrder(Enum):
|
|
45
|
+
"""Sorting options for glob results."""
|
|
46
|
+
|
|
47
|
+
MODIFIED = "modified" # Sort by modification time (newest first)
|
|
48
|
+
SIZE = "size" # Sort by file size (largest first)
|
|
49
|
+
ALPHABETICAL = "alphabetical" # Sort alphabetically
|
|
50
|
+
DEPTH = "depth" # Sort by path depth (shallow first)
|
|
51
|
+
|
|
52
|
+
|
|
38
53
|
class GlobTool(BaseTool):
|
|
39
54
|
"""Fast file pattern matching tool using glob patterns."""
|
|
40
55
|
|
|
56
|
+
def __init__(self):
|
|
57
|
+
"""Initialize the glob tool."""
|
|
58
|
+
super().__init__()
|
|
59
|
+
self._code_index: Optional[CodeIndex] = None
|
|
60
|
+
self._gitignore_patterns: Optional[Set[str]] = None
|
|
61
|
+
|
|
41
62
|
@property
|
|
42
63
|
def tool_name(self) -> str:
|
|
43
64
|
return "glob"
|
|
44
65
|
|
|
66
|
+
@lru_cache(maxsize=1)
|
|
67
|
+
def _get_base_prompt(self) -> str:
|
|
68
|
+
"""Load and return the base prompt from XML file.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str: The loaded prompt from XML or a default prompt
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
# Load prompt from XML file
|
|
75
|
+
prompt_file = Path(__file__).parent / "prompts" / "glob_prompt.xml"
|
|
76
|
+
if prompt_file.exists():
|
|
77
|
+
tree = ET.parse(prompt_file)
|
|
78
|
+
root = tree.getroot()
|
|
79
|
+
description = root.find("description")
|
|
80
|
+
if description is not None:
|
|
81
|
+
return description.text.strip()
|
|
82
|
+
except Exception:
|
|
83
|
+
pass # Fall through to default
|
|
84
|
+
|
|
85
|
+
# Fallback to default prompt
|
|
86
|
+
return """Fast file pattern matching tool
|
|
87
|
+
|
|
88
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
89
|
+
- Returns matching file paths sorted by modification time
|
|
90
|
+
- Use this tool when you need to find files by name patterns"""
|
|
91
|
+
|
|
92
|
+
@lru_cache(maxsize=1)
|
|
93
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
94
|
+
"""Get the parameters schema for the glob tool."""
|
|
95
|
+
# Try to load from XML first
|
|
96
|
+
try:
|
|
97
|
+
prompt_file = Path(__file__).parent / "prompts" / "glob_prompt.xml"
|
|
98
|
+
if prompt_file.exists():
|
|
99
|
+
tree = ET.parse(prompt_file)
|
|
100
|
+
root = tree.getroot()
|
|
101
|
+
parameters = root.find("parameters")
|
|
102
|
+
if parameters is not None:
|
|
103
|
+
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
104
|
+
required_fields: List[str] = []
|
|
105
|
+
|
|
106
|
+
for param in parameters.findall("parameter"):
|
|
107
|
+
name = param.get("name")
|
|
108
|
+
required = param.get("required", "false").lower() == "true"
|
|
109
|
+
param_type = param.find("type")
|
|
110
|
+
description = param.find("description")
|
|
111
|
+
|
|
112
|
+
if name and param_type is not None:
|
|
113
|
+
prop = {
|
|
114
|
+
"type": param_type.text.strip(),
|
|
115
|
+
"description": description.text.strip()
|
|
116
|
+
if description is not None
|
|
117
|
+
else "",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
schema["properties"][name] = prop
|
|
121
|
+
if required:
|
|
122
|
+
required_fields.append(name)
|
|
123
|
+
|
|
124
|
+
schema["required"] = required_fields
|
|
125
|
+
return schema
|
|
126
|
+
except Exception:
|
|
127
|
+
pass # Fall through to hardcoded schema
|
|
128
|
+
|
|
129
|
+
# Fallback to hardcoded schema
|
|
130
|
+
return {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"pattern": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "Glob pattern to match (e.g., '*.py', '**/*.{js,ts}')",
|
|
136
|
+
},
|
|
137
|
+
"directory": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Directory to search in",
|
|
140
|
+
"default": ".",
|
|
141
|
+
},
|
|
142
|
+
"recursive": {
|
|
143
|
+
"type": "boolean",
|
|
144
|
+
"description": "Whether to search recursively",
|
|
145
|
+
"default": True,
|
|
146
|
+
},
|
|
147
|
+
"include_hidden": {
|
|
148
|
+
"type": "boolean",
|
|
149
|
+
"description": "Whether to include hidden files/directories",
|
|
150
|
+
"default": False,
|
|
151
|
+
},
|
|
152
|
+
"exclude_dirs": {
|
|
153
|
+
"type": "array",
|
|
154
|
+
"items": {"type": "string"},
|
|
155
|
+
"description": "Additional directories to exclude from search",
|
|
156
|
+
},
|
|
157
|
+
"max_results": {
|
|
158
|
+
"type": "integer",
|
|
159
|
+
"description": "Maximum number of results to return",
|
|
160
|
+
"default": 5000,
|
|
161
|
+
},
|
|
162
|
+
"sort_by": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"enum": ["modified", "size", "alphabetical", "depth"],
|
|
165
|
+
"description": "How to sort results",
|
|
166
|
+
"default": "modified",
|
|
167
|
+
},
|
|
168
|
+
"case_sensitive": {
|
|
169
|
+
"type": "boolean",
|
|
170
|
+
"description": "Whether pattern matching is case-sensitive",
|
|
171
|
+
"default": False,
|
|
172
|
+
},
|
|
173
|
+
"use_gitignore": {
|
|
174
|
+
"type": "boolean",
|
|
175
|
+
"description": "Whether to respect .gitignore patterns",
|
|
176
|
+
"default": True,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
"required": ["pattern"],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def _get_code_index(self, directory: str) -> Optional[CodeIndex]:
|
|
183
|
+
"""Get the CodeIndex instance if available and appropriate."""
|
|
184
|
+
# Only use CodeIndex if we're searching from the project root
|
|
185
|
+
if directory != "." and directory != os.getcwd():
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
if self._code_index is None:
|
|
189
|
+
try:
|
|
190
|
+
self._code_index = CodeIndex.get_instance()
|
|
191
|
+
# Ensure index is built
|
|
192
|
+
self._code_index.build_index()
|
|
193
|
+
except Exception:
|
|
194
|
+
# CodeIndex not available, fall back to filesystem traversal
|
|
195
|
+
self._code_index = None
|
|
196
|
+
return self._code_index
|
|
197
|
+
|
|
45
198
|
async def _execute(
|
|
46
199
|
self,
|
|
47
200
|
pattern: str,
|
|
@@ -50,6 +203,9 @@ class GlobTool(BaseTool):
|
|
|
50
203
|
include_hidden: bool = False,
|
|
51
204
|
exclude_dirs: Optional[List[str]] = None,
|
|
52
205
|
max_results: int = MAX_RESULTS,
|
|
206
|
+
sort_by: Union[str, SortOrder] = SortOrder.MODIFIED,
|
|
207
|
+
case_sensitive: bool = False,
|
|
208
|
+
use_gitignore: bool = True,
|
|
53
209
|
) -> str:
|
|
54
210
|
"""
|
|
55
211
|
Find files matching glob patterns.
|
|
@@ -61,6 +217,9 @@ class GlobTool(BaseTool):
|
|
|
61
217
|
include_hidden: Whether to include hidden files/directories (default: False)
|
|
62
218
|
exclude_dirs: Additional directories to exclude from search
|
|
63
219
|
max_results: Maximum number of results to return (default: 5000)
|
|
220
|
+
sort_by: How to sort results (modified/size/alphabetical/depth)
|
|
221
|
+
case_sensitive: Whether pattern matching is case-sensitive (default: False)
|
|
222
|
+
use_gitignore: Whether to respect .gitignore patterns (default: True)
|
|
64
223
|
|
|
65
224
|
Returns:
|
|
66
225
|
List of matching file paths as a formatted string
|
|
@@ -85,20 +244,45 @@ class GlobTool(BaseTool):
|
|
|
85
244
|
if exclude_dirs:
|
|
86
245
|
all_exclude_dirs.update(exclude_dirs)
|
|
87
246
|
|
|
247
|
+
# Convert sort_by to enum if string
|
|
248
|
+
if isinstance(sort_by, str):
|
|
249
|
+
try:
|
|
250
|
+
sort_by = SortOrder(sort_by)
|
|
251
|
+
except ValueError:
|
|
252
|
+
sort_by = SortOrder.MODIFIED
|
|
253
|
+
|
|
88
254
|
# Handle multiple extensions pattern like "*.{py,js,ts}"
|
|
89
255
|
patterns = self._expand_brace_pattern(pattern)
|
|
90
256
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
root_path
|
|
94
|
-
|
|
257
|
+
# Load gitignore patterns if requested
|
|
258
|
+
if use_gitignore:
|
|
259
|
+
await self._load_gitignore_patterns(root_path)
|
|
260
|
+
|
|
261
|
+
# Try to use CodeIndex for faster lookup if available
|
|
262
|
+
code_index = self._get_code_index(directory)
|
|
263
|
+
if code_index and not include_hidden and recursive:
|
|
264
|
+
# Use CodeIndex for common cases
|
|
265
|
+
matches = await self._glob_search_with_index(
|
|
266
|
+
code_index, patterns, root_path, all_exclude_dirs, max_results, case_sensitive
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
# Fall back to filesystem traversal
|
|
270
|
+
matches = await self._glob_search(
|
|
271
|
+
root_path,
|
|
272
|
+
patterns,
|
|
273
|
+
recursive,
|
|
274
|
+
include_hidden,
|
|
275
|
+
all_exclude_dirs,
|
|
276
|
+
max_results,
|
|
277
|
+
case_sensitive,
|
|
278
|
+
)
|
|
95
279
|
|
|
96
280
|
# Format results
|
|
97
281
|
if not matches:
|
|
98
282
|
return f"No files found matching pattern: {pattern}"
|
|
99
283
|
|
|
100
|
-
# Sort matches
|
|
101
|
-
matches.
|
|
284
|
+
# Sort matches based on sort_by parameter
|
|
285
|
+
matches = await self._sort_matches(matches, sort_by)
|
|
102
286
|
|
|
103
287
|
# Create formatted output
|
|
104
288
|
output = []
|
|
@@ -128,6 +312,7 @@ class GlobTool(BaseTool):
|
|
|
128
312
|
def _expand_brace_pattern(self, pattern: str) -> List[str]:
|
|
129
313
|
"""
|
|
130
314
|
Expand brace patterns like "*.{py,js,ts}" into multiple patterns.
|
|
315
|
+
Also supports extended patterns like "?(pattern)" for optional matching.
|
|
131
316
|
|
|
132
317
|
Args:
|
|
133
318
|
pattern: Pattern that may contain braces
|
|
@@ -138,24 +323,129 @@ class GlobTool(BaseTool):
|
|
|
138
323
|
if "{" not in pattern or "}" not in pattern:
|
|
139
324
|
return [pattern]
|
|
140
325
|
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
326
|
+
# Handle nested braces recursively
|
|
327
|
+
expanded = []
|
|
328
|
+
stack = [pattern]
|
|
329
|
+
|
|
330
|
+
while stack:
|
|
331
|
+
current = stack.pop()
|
|
332
|
+
|
|
333
|
+
# Find the innermost brace expression
|
|
334
|
+
start = -1
|
|
335
|
+
depth = 0
|
|
336
|
+
for i, char in enumerate(current):
|
|
337
|
+
if char == "{":
|
|
338
|
+
if depth == 0:
|
|
339
|
+
start = i
|
|
340
|
+
depth += 1
|
|
341
|
+
elif char == "}":
|
|
342
|
+
depth -= 1
|
|
343
|
+
if depth == 0 and start != -1:
|
|
344
|
+
# Found a complete brace expression
|
|
345
|
+
prefix = current[:start]
|
|
346
|
+
suffix = current[i + 1 :]
|
|
347
|
+
options = current[start + 1 : i].split(",")
|
|
348
|
+
|
|
349
|
+
# Generate all combinations
|
|
350
|
+
for option in options:
|
|
351
|
+
new_pattern = prefix + option.strip() + suffix
|
|
352
|
+
if "{" in new_pattern:
|
|
353
|
+
stack.append(new_pattern)
|
|
354
|
+
else:
|
|
355
|
+
expanded.append(new_pattern)
|
|
356
|
+
break
|
|
357
|
+
else:
|
|
358
|
+
# No more braces to expand
|
|
359
|
+
expanded.append(current)
|
|
360
|
+
|
|
361
|
+
return expanded
|
|
362
|
+
|
|
363
|
+
async def _load_gitignore_patterns(self, root: Path) -> None:
|
|
364
|
+
"""Load .gitignore patterns from the repository."""
|
|
365
|
+
if self._gitignore_patterns is not None:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
self._gitignore_patterns = set()
|
|
369
|
+
|
|
370
|
+
# Look for .gitignore, .ignore, and .rgignore files
|
|
371
|
+
ignore_files = [".gitignore", ".ignore", ".rgignore"]
|
|
372
|
+
|
|
373
|
+
for ignore_file in ignore_files:
|
|
374
|
+
ignore_path = root / ignore_file
|
|
375
|
+
if ignore_path.exists():
|
|
376
|
+
try:
|
|
377
|
+
with open(ignore_path, "r", encoding="utf-8") as f:
|
|
378
|
+
for line in f:
|
|
379
|
+
line = line.strip()
|
|
380
|
+
if line and not line.startswith("#"):
|
|
381
|
+
self._gitignore_patterns.add(line)
|
|
382
|
+
except Exception:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
async def _glob_search_with_index(
|
|
386
|
+
self,
|
|
387
|
+
code_index: CodeIndex,
|
|
388
|
+
patterns: List[str],
|
|
389
|
+
root: Path,
|
|
390
|
+
exclude_dirs: set,
|
|
391
|
+
max_results: int,
|
|
392
|
+
case_sensitive: bool,
|
|
393
|
+
) -> List[str]:
|
|
394
|
+
"""Use CodeIndex for faster file matching."""
|
|
395
|
+
# Get all files from index
|
|
396
|
+
all_files = code_index.get_all_files()
|
|
397
|
+
|
|
398
|
+
matches = []
|
|
399
|
+
for file_path in all_files:
|
|
400
|
+
# Convert to absolute path
|
|
401
|
+
abs_path = code_index.root_dir / file_path
|
|
402
|
+
|
|
403
|
+
# Check against patterns
|
|
404
|
+
for pattern in patterns:
|
|
405
|
+
if self._match_pattern(str(file_path), pattern, case_sensitive):
|
|
406
|
+
# Check if in excluded directories
|
|
407
|
+
skip = False
|
|
408
|
+
for exclude_dir in exclude_dirs:
|
|
409
|
+
if exclude_dir in file_path.parts:
|
|
410
|
+
skip = True
|
|
411
|
+
break
|
|
412
|
+
|
|
413
|
+
if not skip:
|
|
414
|
+
matches.append(str(abs_path))
|
|
415
|
+
if len(matches) >= max_results:
|
|
416
|
+
return matches
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
return matches
|
|
420
|
+
|
|
421
|
+
def _match_pattern(self, path: str, pattern: str, case_sensitive: bool) -> bool:
|
|
422
|
+
"""Match a path against a glob pattern."""
|
|
423
|
+
# Handle ** for recursive matching
|
|
424
|
+
if "**" in pattern:
|
|
425
|
+
# Special case: **/*.ext should match both root files and nested files
|
|
426
|
+
if pattern.startswith("**/"):
|
|
427
|
+
# Match the pattern after **/ directly and also with any prefix
|
|
428
|
+
suffix_pattern = pattern[3:] # Remove **/
|
|
429
|
+
if case_sensitive:
|
|
430
|
+
# Check if path matches the suffix directly (root files)
|
|
431
|
+
if fnmatch.fnmatch(path, suffix_pattern):
|
|
432
|
+
return True
|
|
433
|
+
else:
|
|
434
|
+
if fnmatch.fnmatch(path.lower(), suffix_pattern.lower()):
|
|
435
|
+
return True
|
|
436
|
+
|
|
437
|
+
# Full recursive pattern matching
|
|
438
|
+
regex_pat = pattern.replace("**", "__STARSTAR__")
|
|
439
|
+
regex_pat = fnmatch.translate(regex_pat)
|
|
440
|
+
regex_pat = regex_pat.replace("__STARSTAR__", ".*")
|
|
441
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
442
|
+
return bool(re.match(regex_pat, path, flags))
|
|
443
|
+
else:
|
|
444
|
+
# Simple pattern matching
|
|
445
|
+
if case_sensitive:
|
|
446
|
+
return fnmatch.fnmatch(path, pattern)
|
|
447
|
+
else:
|
|
448
|
+
return fnmatch.fnmatch(path.lower(), pattern.lower())
|
|
159
449
|
|
|
160
450
|
async def _glob_search(
|
|
161
451
|
self,
|
|
@@ -165,6 +455,7 @@ class GlobTool(BaseTool):
|
|
|
165
455
|
include_hidden: bool,
|
|
166
456
|
exclude_dirs: set,
|
|
167
457
|
max_results: int,
|
|
458
|
+
case_sensitive: bool = False,
|
|
168
459
|
) -> List[str]:
|
|
169
460
|
"""
|
|
170
461
|
Perform the actual glob search using os.scandir for speed.
|
|
@@ -182,14 +473,13 @@ class GlobTool(BaseTool):
|
|
|
182
473
|
"""
|
|
183
474
|
|
|
184
475
|
def search_sync():
|
|
185
|
-
# Import re here to avoid issues at module level
|
|
186
|
-
import re
|
|
187
|
-
|
|
188
476
|
matches = []
|
|
189
477
|
stack = [root]
|
|
190
478
|
|
|
191
479
|
# Compile patterns to regex for faster matching
|
|
192
480
|
compiled_patterns = []
|
|
481
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
482
|
+
|
|
193
483
|
for pat in patterns:
|
|
194
484
|
# Handle ** for recursive matching
|
|
195
485
|
if "**" in pat:
|
|
@@ -197,11 +487,9 @@ class GlobTool(BaseTool):
|
|
|
197
487
|
regex_pat = pat.replace("**", "__STARSTAR__")
|
|
198
488
|
regex_pat = fnmatch.translate(regex_pat)
|
|
199
489
|
regex_pat = regex_pat.replace("__STARSTAR__", ".*")
|
|
200
|
-
compiled_patterns.append((pat, re.compile(regex_pat,
|
|
490
|
+
compiled_patterns.append((pat, re.compile(regex_pat, flags)))
|
|
201
491
|
else:
|
|
202
|
-
compiled_patterns.append(
|
|
203
|
-
(pat, re.compile(fnmatch.translate(pat), re.IGNORECASE))
|
|
204
|
-
)
|
|
492
|
+
compiled_patterns.append((pat, re.compile(fnmatch.translate(pat), flags)))
|
|
205
493
|
|
|
206
494
|
while stack and len(matches) < max_results:
|
|
207
495
|
current_dir = stack.pop()
|
|
@@ -224,9 +512,22 @@ class GlobTool(BaseTool):
|
|
|
224
512
|
for original_pat, compiled_pat in compiled_patterns:
|
|
225
513
|
# For ** patterns, match against full relative path
|
|
226
514
|
if "**" in original_pat:
|
|
227
|
-
|
|
515
|
+
# Special handling for **/*.ext patterns
|
|
516
|
+
if original_pat.startswith("**/") and not recursive:
|
|
517
|
+
# In non-recursive mode, match only filename
|
|
518
|
+
suffix_pat = original_pat[3:]
|
|
519
|
+
if fnmatch.fnmatch(entry.name, suffix_pat):
|
|
520
|
+
matches.append(entry.path)
|
|
521
|
+
break
|
|
522
|
+
elif compiled_pat.match(rel_path):
|
|
228
523
|
matches.append(entry.path)
|
|
229
524
|
break
|
|
525
|
+
# Also check if filename matches the pattern after **/
|
|
526
|
+
elif original_pat.startswith("**/"):
|
|
527
|
+
suffix_pat = original_pat[3:]
|
|
528
|
+
if fnmatch.fnmatch(entry.name, suffix_pat):
|
|
529
|
+
matches.append(entry.path)
|
|
530
|
+
break
|
|
230
531
|
else:
|
|
231
532
|
# For simple patterns, match against filename only
|
|
232
533
|
if compiled_pat.match(entry.name):
|
|
@@ -246,6 +547,28 @@ class GlobTool(BaseTool):
|
|
|
246
547
|
loop = asyncio.get_event_loop()
|
|
247
548
|
return await loop.run_in_executor(None, search_sync)
|
|
248
549
|
|
|
550
|
+
async def _sort_matches(self, matches: List[str], sort_by: SortOrder) -> List[str]:
|
|
551
|
+
"""Sort matches based on the specified order."""
|
|
552
|
+
if not matches:
|
|
553
|
+
return matches
|
|
554
|
+
|
|
555
|
+
def sort_sync():
|
|
556
|
+
if sort_by == SortOrder.MODIFIED:
|
|
557
|
+
# Sort by modification time (newest first)
|
|
558
|
+
return sorted(matches, key=lambda p: os.path.getmtime(p), reverse=True)
|
|
559
|
+
elif sort_by == SortOrder.SIZE:
|
|
560
|
+
# Sort by file size (largest first)
|
|
561
|
+
return sorted(matches, key=lambda p: os.path.getsize(p), reverse=True)
|
|
562
|
+
elif sort_by == SortOrder.DEPTH:
|
|
563
|
+
# Sort by path depth (shallow first), then alphabetically
|
|
564
|
+
return sorted(matches, key=lambda p: (p.count(os.sep), p))
|
|
565
|
+
else: # SortOrder.ALPHABETICAL
|
|
566
|
+
# Sort alphabetically
|
|
567
|
+
return sorted(matches)
|
|
568
|
+
|
|
569
|
+
loop = asyncio.get_event_loop()
|
|
570
|
+
return await loop.run_in_executor(None, sort_sync)
|
|
571
|
+
|
|
249
572
|
|
|
250
573
|
# Create the tool function for pydantic-ai
|
|
251
574
|
async def glob(
|
|
@@ -255,6 +578,9 @@ async def glob(
|
|
|
255
578
|
include_hidden: bool = False,
|
|
256
579
|
exclude_dirs: Optional[List[str]] = None,
|
|
257
580
|
max_results: int = MAX_RESULTS,
|
|
581
|
+
sort_by: str = "modified",
|
|
582
|
+
case_sensitive: bool = False,
|
|
583
|
+
use_gitignore: bool = True,
|
|
258
584
|
) -> str:
|
|
259
585
|
"""
|
|
260
586
|
Find files matching glob patterns with fast filesystem traversal.
|
|
@@ -266,6 +592,9 @@ async def glob(
|
|
|
266
592
|
include_hidden: Whether to include hidden files/directories (default: False)
|
|
267
593
|
exclude_dirs: Additional directories to exclude from search (default: common build/cache dirs)
|
|
268
594
|
max_results: Maximum number of results to return (default: 5000)
|
|
595
|
+
sort_by: How to sort results - "modified", "size", "alphabetical", or "depth" (default: "modified")
|
|
596
|
+
case_sensitive: Whether pattern matching is case-sensitive (default: False)
|
|
597
|
+
use_gitignore: Whether to respect .gitignore patterns (default: True)
|
|
269
598
|
|
|
270
599
|
Returns:
|
|
271
600
|
Formatted list of matching file paths grouped by directory
|
|
@@ -276,6 +605,7 @@ async def glob(
|
|
|
276
605
|
glob("*.{js,ts,jsx,tsx}") # Multiple extensions
|
|
277
606
|
glob("src/**/test_*.py") # Test files in src directory
|
|
278
607
|
glob("**/*.md", include_hidden=True) # Include hidden directories
|
|
608
|
+
glob("*.py", sort_by="size") # Sort by file size
|
|
279
609
|
"""
|
|
280
610
|
tool = GlobTool()
|
|
281
611
|
return await tool._execute(
|
|
@@ -285,4 +615,7 @@ async def glob(
|
|
|
285
615
|
include_hidden=include_hidden,
|
|
286
616
|
exclude_dirs=exclude_dirs,
|
|
287
617
|
max_results=max_results,
|
|
618
|
+
sort_by=sort_by,
|
|
619
|
+
case_sensitive=case_sensitive,
|
|
620
|
+
use_gitignore=use_gitignore,
|
|
288
621
|
)
|