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