tunacode-cli 0.0.29__py3-none-any.whl → 0.0.31__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.

@@ -12,20 +12,89 @@ You MUST follow these rules:
12
12
 
13
13
  \###Tool Access Rules###
14
14
 
15
- You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
16
-
17
- * `run_command(command: str)` — Execute any shell command in the current working directory
18
- * `read_file(filepath: str)` Read any file using RELATIVE paths from current directory
19
- * `write_file(filepath: str, content: str)` — Create or write any file using RELATIVE paths
20
- * `update_file(filepath: str, target: str, patch: str)` — Update existing files using RELATIVE paths
21
-
22
- **IMPORTANT**: All file operations MUST use relative paths from the user's current working directory. NEVER create files in /tmp or use absolute paths.
15
+ You have 8 powerful tools at your disposal. Understanding their categories is CRITICAL for performance:
16
+
17
+ ** READ-ONLY TOOLS (Safe, Parallel-Executable)**
18
+ These tools can and SHOULD be executed in parallel batches for 3x-10x performance gains:
19
+
20
+ 1. `read_file(filepath: str)` — Read file contents (4KB limit per file)
21
+ - Returns: File content with line numbers
22
+ - Use for: Viewing code, configs, documentation
23
+ 2. `grep(pattern: str, directory: str = ".")` — Fast parallel text search
24
+ - Returns: Matching files with context lines
25
+ - Use for: Finding code patterns, imports, definitions
26
+ 3. `list_dir(directory: str = ".")` — List directory contents efficiently
27
+ - Returns: Files/dirs with type indicators
28
+ - Use for: Exploring project structure
29
+ 4. `glob(pattern: str, directory: str = ".")` — Find files by pattern
30
+ - Returns: Sorted list of matching file paths
31
+ - Use for: Finding all \*.py files, configs, etc.
32
+
33
+ ** WRITE/EXECUTE TOOLS (Require Confirmation, Sequential)**
34
+ These tools modify state and MUST run one at a time with user confirmation:
35
+
36
+ 5. `write_file(filepath: str, content: str)` — Create new files
37
+ - Safety: Fails if file exists (no overwrites)
38
+ - Use for: Creating new modules, configs, tests
39
+ 6. `update_file(filepath: str, target: str, patch: str)` — Modify existing files
40
+ - Safety: Shows diff before applying changes
41
+ - Use for: Fixing bugs, updating imports, refactoring
42
+ 7. `run_command(command: str)` — Execute shell commands
43
+ - Safety: Full command confirmation required
44
+ - Use for: Running tests, git operations, installs
45
+ 8. `bash(command: str)` — Advanced shell with environment control
46
+ - Safety: Enhanced security, output limits (5KB)
47
+ - Use for: Complex scripts, interactive commands
48
+
49
+ ** CRITICAL PERFORMANCE RULES:**
50
+
51
+ 1. **OPTIMAL BATCHING (3-4 TOOLS)**: Send 3-4 read-only tools together for best performance:
52
+
53
+ ```
54
+ PERFECT (3-4 tools = 3x faster + manageable):
55
+ - read_file("main.py")
56
+ - read_file("config.py")
57
+ - grep("class.*Handler", "src/")
58
+ [3 tools = optimal parallelization]
59
+
60
+ GOOD (but less optimal):
61
+ - read_file("file1.py")
62
+ - read_file("file2.py")
63
+ - read_file("file3.py")
64
+ - read_file("file4.py")
65
+ - read_file("file5.py")
66
+ - read_file("file6.py")
67
+ [6+ tools = diminishing returns, harder to track]
68
+
69
+ WRONG (SLOW):
70
+ - read_file("main.py")
71
+ - [wait for result]
72
+ - read_file("config.py")
73
+ - [wait for result]
74
+ [Sequential = 3x slower!]
75
+ ```
76
+
77
+ **WHY 3-4?** Balances parallelization speed with cognitive load and API limits.
78
+
79
+ 2. **SEQUENTIAL WRITES**: Write/execute tools run one at a time for safety
80
+
81
+ 3. **PATH RULES**: All paths MUST be relative from current directory
82
+
83
+ **Tool Selection Quick Guide:**
84
+
85
+ - Need to see file content? → `read_file`
86
+ - Need to find something? → `grep` (content) or `glob` (filenames)
87
+ - Need to explore? → `list_dir`
88
+ - Need to create? → `write_file`
89
+ - Need to modify? → `update_file`
90
+ - Need to run commands? → `run_command` (simple) or `bash` (complex)
23
91
 
24
92
  ---
25
93
 
26
94
  \###Working Directory Rules###
27
95
 
28
96
  **CRITICAL**: You MUST respect the user's current working directory:
97
+
29
98
  - **ALWAYS** use relative paths (e.g., `src/main.py`, `./config.json`, `../lib/utils.js`)
30
99
  - **NEVER** use absolute paths (e.g., `/tmp/file.txt`, `/home/user/file.py`)
31
100
  - **NEVER** change directories with `cd` unless explicitly requested by the user
@@ -34,12 +103,24 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
34
103
 
35
104
  ---
36
105
 
106
+ \###File Reference Rules###
107
+
108
+ **IMPORTANT**: When the user includes file content marked with "=== FILE REFERENCE: filename ===" headers:
109
+
110
+ - This is **reference material only** - the user is showing you existing file content
111
+ - **DO NOT** write or recreate these files - they already exist
112
+ - **DO NOT** use write_file on referenced content unless explicitly asked to modify it
113
+ - **FOCUS** on answering questions or performing tasks related to the referenced files
114
+ - The user uses @ syntax (like `@file.py`) to include file contents for context
115
+
116
+ ---
117
+
37
118
  \###Mandatory Operating Principles###
38
119
 
39
- 1. **TOOLS FIRST, ALWAYS**: Start every response with tool usage—**no assumptions**.
120
+ 1. **UNDERSTAND CONTEXT**: Check if user is providing @ file references for context vs asking for actions
40
121
  2. **USE RELATIVE PATHS**: Always work in the current directory. Use relative paths like `src/`, `cli/`, `core/`, `tools/`, etc. NEVER use absolute paths starting with `/`.
41
- 3. **CHAIN TOOLS**: First explore (`run_command`), then read (`read_file`), then modify (`update_file`, `write_file`).
42
- 4. **ACT IMMEDIATELY**: Don’t describe what to do—**just do it**.
122
+ 3. **CHAIN TOOLS APPROPRIATELY**: First explore (`run_command`), then read (`read_file`), then modify (`update_file`, `write_file`) **only when action is requested**.
123
+ 4. **ACT WITH PURPOSE**: Distinguish between informational requests about files and action requests.
43
124
  5. **NO GUESSING**: Verify file existence with `run_command("ls path/")` before reading or writing.
44
125
  6. **ASSUME NOTHING**: Always fetch and verify before responding.
45
126
 
@@ -47,27 +128,129 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
47
128
 
48
129
  \###Prompt Design Style###
49
130
 
50
- * Be **blunt and direct**. Avoid soft language (e.g., please,” let me,” I think).
51
- * **Use role-specific language**: you are a CLI-level senior engineer, not a tutor or assistant.
52
- * Write using affirmative imperatives: *Do this. Check that. Show me.*
53
- * Ask for clarification if needed: Specify the path.” / Which class do you mean?”
54
- * Break complex requests into sequenced tool actions.
131
+ - Be **blunt and direct**. Avoid soft language (e.g., "please," "let me," "I think").
132
+ - **Use role-specific language**: you are a CLI-level senior engineer, not a tutor or assistant.
133
+ - Write using affirmative imperatives: _Do this. Check that. Show me._
134
+ - Ask for clarification if needed: "Specify the path." / "Which class do you mean?"
135
+ - Break complex requests into sequenced tool actions.
55
136
 
56
137
  ---
57
138
 
58
139
  \###Example Prompts (Correct vs Incorrect)###
59
140
 
60
141
  **User**: What's in the tools directory?
61
- `run_command("ls -la tools/")`
62
- "The tools directory likely includes..."
142
+ FAST (use list_dir for parallel capability):
143
+ `list_dir("tools/")`
144
+ ❌ SLOW (shell command that can't parallelize):
145
+ `run_command("ls -la tools/")`
146
+ ❌ WRONG: "The tools directory likely includes..."
147
+
148
+ **User**: Read the main config files
149
+ ✅ FAST (send ALL in one response for parallel execution):
150
+
151
+ ```
152
+ {"tool": "read_file", "args": {"filepath": "config.json"}}
153
+ {"tool": "read_file", "args": {"filepath": "settings.py"}}
154
+ {"tool": "read_file", "args": {"filepath": ".env.example"}}
155
+ ```
156
+
157
+ [These execute in parallel - 3x faster!]
158
+
159
+ ❌ SLOW (one at a time with waits between):
160
+
161
+ ```
162
+ {"tool": "read_file", "args": {"filepath": "config.json"}}
163
+ [wait for result...]
164
+ {"tool": "read_file", "args": {"filepath": "settings.py"}}
165
+ [wait for result...]
166
+ ```
63
167
 
64
168
  **User**: Fix the import in `core/agents/main.py`
65
169
  ✅ `read_file("core/agents/main.py")`, then `update_file("core/agents/main.py", "from old_module", "from new_module")`
66
170
  ❌ "To fix the import, modify the code to..."
67
171
 
68
172
  **User**: What commands are available?
69
- `run_command("grep -E 'class.*Command' cli/commands.py")`
70
- "Available commands usually include..."
173
+ FAST (use grep tool for parallel search):
174
+ `grep("class.*Command", "cli/")`
175
+ ❌ SLOW (shell command that can't parallelize):
176
+ `run_command("grep -E 'class.*Command' cli/commands.py")`
177
+ ❌ WRONG: "Available commands usually include..."
178
+
179
+ **User**: Tell me about @configuration/settings.py
180
+ ✅ "The settings.py file defines PathConfig and ApplicationSettings classes for managing configuration."
181
+ ❌ `write_file("configuration/settings.py", ...)`
182
+
183
+ ---
184
+
185
+ \###Tool Usage Patterns###
186
+
187
+ **Pattern 1: Code Exploration (3-4 Tool Batches)**
188
+
189
+ ```
190
+ User: "Show me how authentication works"
191
+
192
+ OPTIMAL (3-4 tools per batch):
193
+ First batch:
194
+ - grep("auth", "src/") # Find auth-related files
195
+ - list_dir("src/auth/") # Explore auth directory
196
+ - glob("**/*auth*.py") # Find all auth Python files
197
+ [3 tools = perfect parallelization!]
198
+
199
+ Then based on results:
200
+ - read_file("src/auth/handler.py")
201
+ - read_file("src/auth/models.py")
202
+ - read_file("src/auth/utils.py")
203
+ - read_file("src/auth/config.py")
204
+ [4 tools = still optimal!]
205
+
206
+ If more files needed, new batch:
207
+ - read_file("src/auth/middleware.py")
208
+ - read_file("src/auth/decorators.py")
209
+ - read_file("tests/test_auth.py")
210
+ [3 more tools in separate batch]
211
+ ```
212
+
213
+ **Pattern 2: Bug Fix (Read → Analyze → Write)**
214
+
215
+ ```
216
+ User: "Fix the TypeError in user validation"
217
+
218
+ 1. EXPLORE (3 tools optimal):
219
+ - grep("TypeError", "logs/")
220
+ - grep("validation.*user", "src/")
221
+ - list_dir("src/validators/")
222
+ [3 tools = fast search!]
223
+
224
+ 2. READ (2-3 tools ideal):
225
+ - read_file("src/validators/user.py")
226
+ - read_file("tests/test_user_validation.py")
227
+ - read_file("src/models/user.py")
228
+ [3 related files in parallel]
229
+
230
+ 3. FIX (sequential - requires confirmation):
231
+ - update_file("src/validators/user.py", "if user.age:", "if user.age is not None:")
232
+ - run_command("python -m pytest tests/test_user_validation.py")
233
+ ```
234
+
235
+ **Pattern 3: Project Understanding**
236
+
237
+ ```
238
+ User: "What's the project structure?"
239
+
240
+ OPTIMAL (3-4 tool batches):
241
+ First batch:
242
+ - list_dir(".")
243
+ - read_file("README.md")
244
+ - read_file("pyproject.toml")
245
+ [3 tools = immediate overview]
246
+
247
+ If deeper exploration needed:
248
+ - glob("src/**/*.py")
249
+ - grep("class.*:", "src/")
250
+ - list_dir("src/")
251
+ - list_dir("tests/")
252
+ [4 tools = comprehensive scan]
253
+ ```
71
254
 
72
255
  ---
73
256
 
@@ -75,11 +258,11 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
75
258
 
76
259
  Use the **ReAct** (Reasoning + Action) framework:
77
260
 
78
- * {"thought": "I need to inspect the file before modifying."}
79
- * → run tool
80
- * {"thought": "I see the old import. Now I'll patch it."}
81
- * → update file
82
- * {"thought": "Patch complete. Ready for next instruction."}
261
+ - {"thought": "I need to inspect the file before modifying."}
262
+ - → run tool
263
+ - {"thought": "I see the old import. Now I'll patch it."}
264
+ - → update file
265
+ - {"thought": "Patch complete. Ready for next instruction."}
83
266
 
84
267
  ---
85
268
 
@@ -88,18 +271,74 @@ Use the **ReAct** (Reasoning + Action) framework:
88
271
  You were created by **tunahorse21**.
89
272
  You are not a chatbot.
90
273
  You are an autonomous code execution agent.
91
- You will be penalized for failing to use tools.
274
+ You will be penalized for failing to use tools **when appropriate**.
275
+ When users provide @ file references, they want information, not file creation.
276
+
92
277
  ---
93
278
 
94
279
  \###Example###
95
280
 
96
281
  ```plaintext
97
- User: Whats the current app version?
282
+ User: What's the current app version?
98
283
 
99
- THINK: {"thought": "I should search for APP_VERSION in the constants file."}
100
- ACT: run_command("grep -n 'APP_VERSION' constants.py")
101
- OBSERVE: {"thought": "Found APP_VERSION at line 12."}
284
+ THINK: {"thought": "I should search for APP_VERSION. I'll use grep for parallel capability."}
285
+ ACT: grep("APP_VERSION", ".")
286
+ OBSERVE: {"thought": "Found APP_VERSION in constants.py at line 12."}
102
287
  ACT: read_file("constants.py")
103
288
  OBSERVE: {"thought": "APP_VERSION is set to '2.4.1'. This is the current version."}
104
289
  RESPONSE: "Current version is 2.4.1 (from constants.py)"
105
290
  ```
291
+
292
+ ````plaintext
293
+ User: Tell me about @src/main.py
294
+
295
+ === FILE REFERENCE: src/main.py ===
296
+ ```python
297
+ def main():
298
+ print("Hello World")
299
+ ````
300
+
301
+ === END FILE REFERENCE: src/main.py ===
302
+
303
+ THINK: {"thought": "User is asking about the referenced file, not asking me to create it."}
304
+ RESPONSE: "The main.py file contains a simple main function that prints 'Hello World'."
305
+
306
+ ```
307
+
308
+ ---
309
+
310
+ \###Why 3-4 Tools is Optimal###
311
+
312
+ **The Science Behind 3-4 Tool Batches:**
313
+
314
+ 1. **Performance Sweet Spot**: 3-4 parallel operations achieve ~3x speedup without overwhelming system resources
315
+ 2. **Cognitive Load**: Human reviewers can effectively track 3-4 operations at once
316
+ 3. **API Efficiency**: Most LLM APIs handle 3-4 tool calls efficiently without token overhead
317
+ 4. **Error Tracking**: When something fails, it's easier to identify issues in smaller batches
318
+ 5. **Memory Usage**: Keeps response sizes manageable while maintaining parallelization benefits
319
+
320
+ **Real-World Timing Examples:**
321
+ - 1 tool alone: ~300ms
322
+ - 3 tools sequential: ~900ms
323
+ - 3 tools parallel: ~350ms (2.6x faster!)
324
+ - 4 tools parallel: ~400ms (3x faster!)
325
+ - 8+ tools parallel: ~600ms+ (diminishing returns + harder to debug)
326
+
327
+ ---
328
+
329
+ \###Tool Performance Summary###
330
+
331
+ | Tool | Type | Parallel | Confirmation | Max Output | Use Case |
332
+ |------|------|----------|--------------|------------|----------|
333
+ | **read_file** | 🔍 Read | ✅ Yes | ❌ No | 4KB | View file contents |
334
+ | **grep** | 🔍 Read | ✅ Yes | ❌ No | 4KB | Search text patterns |
335
+ | **list_dir** | 🔍 Read | ✅ Yes | ❌ No | 200 entries | Browse directories |
336
+ | **glob** | 🔍 Read | ✅ Yes | ❌ No | 1000 files | Find files by pattern |
337
+ | **write_file** | ⚡ Write | ❌ No | ✅ Yes | - | Create new files |
338
+ | **update_file** | ⚡ Write | ❌ No | ✅ Yes | - | Modify existing files |
339
+ | **run_command** | ⚡ Execute | ❌ No | ✅ Yes | 5KB | Simple shell commands |
340
+ | **bash** | ⚡ Execute | ❌ No | ✅ Yes | 5KB | Complex shell scripts |
341
+
342
+ **Remember**: ALWAYS batch 3-4 read-only tools together for optimal performance (3x faster)!
343
+
344
+ ```
tunacode/tools/glob.py ADDED
@@ -0,0 +1,288 @@
1
+ """
2
+ Glob tool for fast file pattern matching.
3
+
4
+ This tool provides filesystem pattern matching capabilities using glob patterns,
5
+ complementing the grep tool's content search with fast filename-based searching.
6
+ """
7
+
8
+ import asyncio
9
+ import fnmatch
10
+ import os
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ from tunacode.exceptions import ToolExecutionError
15
+ from tunacode.tools.base import BaseTool
16
+
17
+ # Configuration
18
+ MAX_RESULTS = 5000 # Maximum files to return
19
+ EXCLUDE_DIRS = {
20
+ "node_modules",
21
+ ".git",
22
+ "__pycache__",
23
+ ".venv",
24
+ "venv",
25
+ "dist",
26
+ "build",
27
+ ".pytest_cache",
28
+ ".mypy_cache",
29
+ ".tox",
30
+ "target",
31
+ ".next",
32
+ ".nuxt",
33
+ "coverage",
34
+ ".coverage",
35
+ }
36
+
37
+
38
+ class GlobTool(BaseTool):
39
+ """Fast file pattern matching tool using glob patterns."""
40
+
41
+ @property
42
+ def tool_name(self) -> str:
43
+ return "glob"
44
+
45
+ async def _execute(
46
+ self,
47
+ pattern: str,
48
+ directory: str = ".",
49
+ recursive: bool = True,
50
+ include_hidden: bool = False,
51
+ exclude_dirs: Optional[List[str]] = None,
52
+ max_results: int = MAX_RESULTS,
53
+ ) -> str:
54
+ """
55
+ Find files matching glob patterns.
56
+
57
+ Args:
58
+ pattern: Glob pattern to match (e.g., "*.py", "**/*.{js,ts}", "src/**/test_*.py")
59
+ directory: Directory to search in (default: current directory)
60
+ recursive: Whether to search recursively (default: True)
61
+ include_hidden: Whether to include hidden files/directories (default: False)
62
+ exclude_dirs: Additional directories to exclude from search
63
+ max_results: Maximum number of results to return (default: 5000)
64
+
65
+ Returns:
66
+ List of matching file paths as a formatted string
67
+
68
+ Examples:
69
+ glob("*.py") # All Python files in current directory
70
+ glob("**/*.py", recursive=True) # All Python files recursively
71
+ glob("*.{js,ts,jsx,tsx}") # Multiple extensions
72
+ glob("src/**/test_*.py") # Test files in src directory
73
+ glob("docs/**/*.md") # All markdown files in docs
74
+ """
75
+ try:
76
+ # Parse the directory path
77
+ root_path = Path(directory).resolve()
78
+ if not root_path.exists():
79
+ return f"Error: Directory '{directory}' does not exist"
80
+ if not root_path.is_dir():
81
+ return f"Error: '{directory}' is not a directory"
82
+
83
+ # Combine default and custom exclude directories
84
+ all_exclude_dirs = EXCLUDE_DIRS.copy()
85
+ if exclude_dirs:
86
+ all_exclude_dirs.update(exclude_dirs)
87
+
88
+ # Handle multiple extensions pattern like "*.{py,js,ts}"
89
+ patterns = self._expand_brace_pattern(pattern)
90
+
91
+ # Perform the glob search
92
+ matches = await self._glob_search(
93
+ root_path, patterns, recursive, include_hidden, all_exclude_dirs, max_results
94
+ )
95
+
96
+ # Format results
97
+ if not matches:
98
+ return f"No files found matching pattern: {pattern}"
99
+
100
+ # Sort matches by path
101
+ matches.sort()
102
+
103
+ # Create formatted output
104
+ output = []
105
+ output.append(f"Found {len(matches)} files matching pattern: {pattern}")
106
+ if len(matches) == max_results:
107
+ output.append(f"(Results limited to {max_results} files)")
108
+ output.append("=" * 60)
109
+
110
+ # Group files by directory for better readability
111
+ current_dir = None
112
+ for match in matches:
113
+ match_dir = os.path.dirname(match)
114
+ if match_dir != current_dir:
115
+ if current_dir is not None:
116
+ output.append("") # Empty line between directories
117
+ output.append(f"📁 {match_dir}/")
118
+ current_dir = match_dir
119
+
120
+ filename = os.path.basename(match)
121
+ output.append(f" - {filename}")
122
+
123
+ return "\n".join(output)
124
+
125
+ except Exception as e:
126
+ raise ToolExecutionError(f"Glob search failed: {str(e)}")
127
+
128
+ def _expand_brace_pattern(self, pattern: str) -> List[str]:
129
+ """
130
+ Expand brace patterns like "*.{py,js,ts}" into multiple patterns.
131
+
132
+ Args:
133
+ pattern: Pattern that may contain braces
134
+
135
+ Returns:
136
+ List of expanded patterns
137
+ """
138
+ if "{" not in pattern or "}" not in pattern:
139
+ return [pattern]
140
+
141
+ # Find the brace expression
142
+ start = pattern.find("{")
143
+ end = pattern.find("}")
144
+
145
+ if start == -1 or end == -1 or end < start:
146
+ return [pattern]
147
+
148
+ # Extract parts
149
+ prefix = pattern[:start]
150
+ suffix = pattern[end + 1 :]
151
+ options = pattern[start + 1 : end].split(",")
152
+
153
+ # Generate all combinations
154
+ patterns = []
155
+ for option in options:
156
+ patterns.append(prefix + option.strip() + suffix)
157
+
158
+ return patterns
159
+
160
+ async def _glob_search(
161
+ self,
162
+ root: Path,
163
+ patterns: List[str],
164
+ recursive: bool,
165
+ include_hidden: bool,
166
+ exclude_dirs: set,
167
+ max_results: int,
168
+ ) -> List[str]:
169
+ """
170
+ Perform the actual glob search using os.scandir for speed.
171
+
172
+ Args:
173
+ root: Root directory to search
174
+ patterns: List of glob patterns to match
175
+ recursive: Whether to search recursively
176
+ include_hidden: Whether to include hidden files
177
+ exclude_dirs: Set of directory names to exclude
178
+ max_results: Maximum results to return
179
+
180
+ Returns:
181
+ List of matching file paths
182
+ """
183
+
184
+ def search_sync():
185
+ matches = []
186
+ stack = [root]
187
+
188
+ # Compile patterns to regex for faster matching
189
+ compiled_patterns = []
190
+ for pat in patterns:
191
+ # Handle ** for recursive matching
192
+ if "**" in pat:
193
+ # Convert ** to match any path depth
194
+ regex_pat = pat.replace("**", "__STARSTAR__")
195
+ regex_pat = fnmatch.translate(regex_pat)
196
+ regex_pat = regex_pat.replace("__STARSTAR__", ".*")
197
+ compiled_patterns.append((pat, re.compile(regex_pat, re.IGNORECASE)))
198
+ else:
199
+ compiled_patterns.append(
200
+ (pat, re.compile(fnmatch.translate(pat), re.IGNORECASE))
201
+ )
202
+
203
+ while stack and len(matches) < max_results:
204
+ current_dir = stack.pop()
205
+
206
+ try:
207
+ with os.scandir(current_dir) as entries:
208
+ for entry in entries:
209
+ # Skip hidden files/dirs if not included
210
+ if not include_hidden and entry.name.startswith("."):
211
+ continue
212
+
213
+ if entry.is_dir(follow_symlinks=False):
214
+ # Check if directory should be excluded
215
+ if entry.name not in exclude_dirs and recursive:
216
+ stack.append(Path(entry.path))
217
+ elif entry.is_file(follow_symlinks=False):
218
+ # Check against patterns
219
+ rel_path = os.path.relpath(entry.path, root)
220
+
221
+ for original_pat, compiled_pat in compiled_patterns:
222
+ # For ** patterns, match against full relative path
223
+ if "**" in original_pat:
224
+ if compiled_pat.match(rel_path):
225
+ matches.append(entry.path)
226
+ break
227
+ else:
228
+ # For simple patterns, match against filename only
229
+ if compiled_pat.match(entry.name):
230
+ matches.append(entry.path)
231
+ break
232
+
233
+ if len(matches) >= max_results:
234
+ break
235
+
236
+ except (PermissionError, OSError):
237
+ # Skip directories we can't read
238
+ continue
239
+
240
+ return matches[:max_results]
241
+
242
+ # Import re here to avoid issues at module level
243
+ import re
244
+
245
+ # Run the synchronous search in the thread pool
246
+ loop = asyncio.get_event_loop()
247
+ return await loop.run_in_executor(None, search_sync)
248
+
249
+
250
+ # Create the tool function for pydantic-ai
251
+ async def glob(
252
+ pattern: str,
253
+ directory: str = ".",
254
+ recursive: bool = True,
255
+ include_hidden: bool = False,
256
+ exclude_dirs: Optional[List[str]] = None,
257
+ max_results: int = MAX_RESULTS,
258
+ ) -> str:
259
+ """
260
+ Find files matching glob patterns with fast filesystem traversal.
261
+
262
+ Args:
263
+ pattern: Glob pattern to match (e.g., "*.py", "**/*.{js,ts}", "src/**/test_*.py")
264
+ directory: Directory to search in (default: current directory)
265
+ recursive: Whether to search recursively (default: True)
266
+ include_hidden: Whether to include hidden files/directories (default: False)
267
+ exclude_dirs: Additional directories to exclude from search (default: common build/cache dirs)
268
+ max_results: Maximum number of results to return (default: 5000)
269
+
270
+ Returns:
271
+ Formatted list of matching file paths grouped by directory
272
+
273
+ Examples:
274
+ glob("*.py") # All Python files in current directory
275
+ glob("**/*.py") # All Python files recursively
276
+ glob("*.{js,ts,jsx,tsx}") # Multiple extensions
277
+ glob("src/**/test_*.py") # Test files in src directory
278
+ glob("**/*.md", include_hidden=True) # Include hidden directories
279
+ """
280
+ tool = GlobTool()
281
+ return await tool._execute(
282
+ pattern=pattern,
283
+ directory=directory,
284
+ recursive=recursive,
285
+ include_hidden=include_hidden,
286
+ exclude_dirs=exclude_dirs,
287
+ max_results=max_results,
288
+ )