tunacode-cli 0.0.30__py3-none-any.whl → 0.0.32__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.
- api/auth.py +13 -0
- api/users.py +8 -0
- tunacode/cli/commands.py +113 -232
- tunacode/cli/repl.py +40 -84
- tunacode/constants.py +10 -1
- tunacode/core/agents/__init__.py +0 -4
- tunacode/core/agents/main.py +345 -43
- tunacode/core/code_index.py +479 -0
- tunacode/core/setup/git_safety_setup.py +7 -9
- tunacode/core/tool_handler.py +18 -0
- tunacode/exceptions.py +13 -0
- tunacode/prompts/system.md +237 -28
- tunacode/tools/glob.py +288 -0
- tunacode/tools/grep.py +168 -195
- tunacode/tools/list_dir.py +190 -0
- tunacode/tools/read_file.py +9 -3
- tunacode/tools/read_file_async_poc.py +188 -0
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/METADATA +16 -7
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/RECORD +23 -21
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/top_level.txt +1 -0
- tunacode/core/agents/orchestrator.py +0 -213
- tunacode/core/agents/planner_schema.py +0 -9
- tunacode/core/agents/readonly.py +0 -65
- tunacode/core/llm/planner.py +0 -62
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.30.dist-info → tunacode_cli-0.0.32.dist-info}/licenses/LICENSE +0 -0
tunacode/prompts/system.md
CHANGED
|
@@ -12,20 +12,89 @@ You MUST follow these rules:
|
|
|
12
12
|
|
|
13
13
|
\###Tool Access Rules###
|
|
14
14
|
|
|
15
|
-
You
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
@@ -37,6 +106,7 @@ You HAVE the following tools available. USE THEM WHEN APPROPRIATE:
|
|
|
37
106
|
\###File Reference Rules###
|
|
38
107
|
|
|
39
108
|
**IMPORTANT**: When the user includes file content marked with "=== FILE REFERENCE: filename ===" headers:
|
|
109
|
+
|
|
40
110
|
- This is **reference material only** - the user is showing you existing file content
|
|
41
111
|
- **DO NOT** write or recreate these files - they already exist
|
|
42
112
|
- **DO NOT** use write_file on referenced content unless explicitly asked to modify it
|
|
@@ -58,27 +128,53 @@ You HAVE the following tools available. USE THEM WHEN APPROPRIATE:
|
|
|
58
128
|
|
|
59
129
|
\###Prompt Design Style###
|
|
60
130
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
66
136
|
|
|
67
137
|
---
|
|
68
138
|
|
|
69
139
|
\###Example Prompts (Correct vs Incorrect)###
|
|
70
140
|
|
|
71
141
|
**User**: What's in the tools directory?
|
|
72
|
-
✅
|
|
73
|
-
|
|
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
|
+
```
|
|
74
167
|
|
|
75
168
|
**User**: Fix the import in `core/agents/main.py`
|
|
76
169
|
✅ `read_file("core/agents/main.py")`, then `update_file("core/agents/main.py", "from old_module", "from new_module")`
|
|
77
170
|
❌ "To fix the import, modify the code to..."
|
|
78
171
|
|
|
79
172
|
**User**: What commands are available?
|
|
80
|
-
✅
|
|
81
|
-
|
|
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..."
|
|
82
178
|
|
|
83
179
|
**User**: Tell me about @configuration/settings.py
|
|
84
180
|
✅ "The settings.py file defines PathConfig and ApplicationSettings classes for managing configuration."
|
|
@@ -86,15 +182,87 @@ You HAVE the following tools available. USE THEM WHEN APPROPRIATE:
|
|
|
86
182
|
|
|
87
183
|
---
|
|
88
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
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
89
257
|
\###Meta Behavior###
|
|
90
258
|
|
|
91
259
|
Use the **ReAct** (Reasoning + Action) framework:
|
|
92
260
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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."}
|
|
98
266
|
|
|
99
267
|
---
|
|
100
268
|
|
|
@@ -105,6 +273,7 @@ You are not a chatbot.
|
|
|
105
273
|
You are an autonomous code execution agent.
|
|
106
274
|
You will be penalized for failing to use tools **when appropriate**.
|
|
107
275
|
When users provide @ file references, they want information, not file creation.
|
|
276
|
+
|
|
108
277
|
---
|
|
109
278
|
|
|
110
279
|
\###Example###
|
|
@@ -112,24 +281,64 @@ When users provide @ file references, they want information, not file creation.
|
|
|
112
281
|
```plaintext
|
|
113
282
|
User: What's the current app version?
|
|
114
283
|
|
|
115
|
-
THINK: {"thought": "I should search for APP_VERSION
|
|
116
|
-
ACT:
|
|
117
|
-
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."}
|
|
118
287
|
ACT: read_file("constants.py")
|
|
119
288
|
OBSERVE: {"thought": "APP_VERSION is set to '2.4.1'. This is the current version."}
|
|
120
289
|
RESPONSE: "Current version is 2.4.1 (from constants.py)"
|
|
121
290
|
```
|
|
122
291
|
|
|
123
|
-
|
|
292
|
+
````plaintext
|
|
124
293
|
User: Tell me about @src/main.py
|
|
125
294
|
|
|
126
295
|
=== FILE REFERENCE: src/main.py ===
|
|
127
296
|
```python
|
|
128
297
|
def main():
|
|
129
298
|
print("Hello World")
|
|
130
|
-
|
|
299
|
+
````
|
|
300
|
+
|
|
131
301
|
=== END FILE REFERENCE: src/main.py ===
|
|
132
302
|
|
|
133
303
|
THINK: {"thought": "User is asking about the referenced file, not asking me to create it."}
|
|
134
304
|
RESPONSE: "The main.py file contains a simple main function that prints 'Hello World'."
|
|
135
|
-
|
|
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
|
+
)
|