tunacode-cli 0.1.21__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/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
tunacode/tools/glob.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Glob tool for fast file pattern matching."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import fnmatch
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from tunacode.indexing import CodeIndex
|
|
11
|
+
from tunacode.tools.decorators import base_tool
|
|
12
|
+
|
|
13
|
+
MAX_RESULTS = 5000
|
|
14
|
+
EXCLUDE_DIRS = {
|
|
15
|
+
"node_modules",
|
|
16
|
+
".git",
|
|
17
|
+
"__pycache__",
|
|
18
|
+
".venv",
|
|
19
|
+
"venv",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
".mypy_cache",
|
|
24
|
+
".tox",
|
|
25
|
+
"target",
|
|
26
|
+
".next",
|
|
27
|
+
".nuxt",
|
|
28
|
+
"coverage",
|
|
29
|
+
".coverage",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SortOrder(Enum):
|
|
34
|
+
"""Sorting options for glob results."""
|
|
35
|
+
|
|
36
|
+
MODIFIED = "modified"
|
|
37
|
+
SIZE = "size"
|
|
38
|
+
ALPHABETICAL = "alphabetical"
|
|
39
|
+
DEPTH = "depth"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Module-level cache for gitignore patterns
|
|
43
|
+
_gitignore_patterns: set[str] | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@base_tool
|
|
47
|
+
async def glob(
|
|
48
|
+
pattern: str,
|
|
49
|
+
directory: str = ".",
|
|
50
|
+
recursive: bool = True,
|
|
51
|
+
include_hidden: bool = False,
|
|
52
|
+
exclude_dirs: list[str] | None = None,
|
|
53
|
+
max_results: int = MAX_RESULTS,
|
|
54
|
+
sort_by: str = "modified",
|
|
55
|
+
case_sensitive: bool = False,
|
|
56
|
+
use_gitignore: bool = True,
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Find files matching glob patterns.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
pattern: Glob pattern to match (e.g., "*.py", "**/*.{js,ts}").
|
|
62
|
+
directory: Directory to search in (default: current directory).
|
|
63
|
+
recursive: Whether to search recursively (default: True).
|
|
64
|
+
include_hidden: Whether to include hidden files/directories.
|
|
65
|
+
exclude_dirs: Additional directories to exclude from search.
|
|
66
|
+
max_results: Maximum number of results to return.
|
|
67
|
+
sort_by: How to sort results (modified/size/alphabetical/depth).
|
|
68
|
+
case_sensitive: Whether pattern matching is case-sensitive.
|
|
69
|
+
use_gitignore: Whether to respect .gitignore patterns.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Formatted list of matching file paths.
|
|
73
|
+
"""
|
|
74
|
+
root_path = Path(directory).resolve()
|
|
75
|
+
if not root_path.exists():
|
|
76
|
+
return f"Error: Directory '{directory}' does not exist"
|
|
77
|
+
if not root_path.is_dir():
|
|
78
|
+
return f"Error: '{directory}' is not a directory"
|
|
79
|
+
|
|
80
|
+
all_exclude = EXCLUDE_DIRS.copy()
|
|
81
|
+
if exclude_dirs:
|
|
82
|
+
all_exclude.update(exclude_dirs)
|
|
83
|
+
|
|
84
|
+
sort_order = _parse_sort_order(sort_by)
|
|
85
|
+
patterns = _expand_brace_pattern(pattern)
|
|
86
|
+
|
|
87
|
+
if use_gitignore:
|
|
88
|
+
await _load_gitignore_patterns(root_path)
|
|
89
|
+
|
|
90
|
+
# Try CodeIndex for faster lookup
|
|
91
|
+
code_index = _get_code_index(directory)
|
|
92
|
+
source = "filesystem"
|
|
93
|
+
|
|
94
|
+
if code_index and not include_hidden and recursive:
|
|
95
|
+
matches = await _glob_with_index(
|
|
96
|
+
code_index, patterns, root_path, all_exclude, max_results, case_sensitive
|
|
97
|
+
)
|
|
98
|
+
source = "index"
|
|
99
|
+
else:
|
|
100
|
+
matches = await _glob_filesystem(
|
|
101
|
+
root_path, patterns, recursive, include_hidden, all_exclude, max_results, case_sensitive
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not matches:
|
|
105
|
+
return f"[source:{source}]\nNo files found matching pattern: {pattern}"
|
|
106
|
+
|
|
107
|
+
matches = await _sort_matches(matches, sort_order)
|
|
108
|
+
return _format_output(pattern, matches, max_results, source)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_sort_order(sort_by: str) -> SortOrder:
|
|
112
|
+
"""Parse sort order string to enum."""
|
|
113
|
+
try:
|
|
114
|
+
return SortOrder(sort_by)
|
|
115
|
+
except ValueError:
|
|
116
|
+
return SortOrder.MODIFIED
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_code_index(directory: str) -> CodeIndex | None:
|
|
120
|
+
"""Get CodeIndex instance if searching from project root."""
|
|
121
|
+
if directory != "." and directory != os.getcwd():
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
index = CodeIndex.get_instance()
|
|
125
|
+
index.build_index()
|
|
126
|
+
return index
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _expand_brace_pattern(pattern: str) -> list[str]:
|
|
132
|
+
"""Expand brace patterns like "*.{py,js,ts}" into multiple patterns."""
|
|
133
|
+
if "{" not in pattern or "}" not in pattern:
|
|
134
|
+
return [pattern]
|
|
135
|
+
|
|
136
|
+
expanded = []
|
|
137
|
+
stack = [pattern]
|
|
138
|
+
|
|
139
|
+
while stack:
|
|
140
|
+
current = stack.pop()
|
|
141
|
+
start = -1
|
|
142
|
+
depth = 0
|
|
143
|
+
|
|
144
|
+
for i, char in enumerate(current):
|
|
145
|
+
if char == "{":
|
|
146
|
+
if depth == 0:
|
|
147
|
+
start = i
|
|
148
|
+
depth += 1
|
|
149
|
+
elif char == "}":
|
|
150
|
+
depth -= 1
|
|
151
|
+
if depth == 0 and start != -1:
|
|
152
|
+
prefix = current[:start]
|
|
153
|
+
suffix = current[i + 1 :]
|
|
154
|
+
options = current[start + 1 : i].split(",")
|
|
155
|
+
|
|
156
|
+
for option in options:
|
|
157
|
+
new_pattern = prefix + option.strip() + suffix
|
|
158
|
+
if "{" in new_pattern:
|
|
159
|
+
stack.append(new_pattern)
|
|
160
|
+
else:
|
|
161
|
+
expanded.append(new_pattern)
|
|
162
|
+
break
|
|
163
|
+
else:
|
|
164
|
+
expanded.append(current)
|
|
165
|
+
|
|
166
|
+
return expanded
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _load_gitignore_patterns(root: Path) -> None:
|
|
170
|
+
"""Load .gitignore patterns from the repository."""
|
|
171
|
+
global _gitignore_patterns
|
|
172
|
+
if _gitignore_patterns is not None:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
_gitignore_patterns = set()
|
|
176
|
+
ignore_files = [".gitignore", ".ignore", ".rgignore"]
|
|
177
|
+
|
|
178
|
+
for ignore_file in ignore_files:
|
|
179
|
+
ignore_path = root / ignore_file
|
|
180
|
+
if ignore_path.exists():
|
|
181
|
+
try:
|
|
182
|
+
with open(ignore_path, encoding="utf-8") as f:
|
|
183
|
+
for line in f:
|
|
184
|
+
line = line.strip()
|
|
185
|
+
if line and not line.startswith("#"):
|
|
186
|
+
_gitignore_patterns.add(line)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def _glob_with_index(
|
|
192
|
+
code_index: CodeIndex,
|
|
193
|
+
patterns: list[str],
|
|
194
|
+
root: Path,
|
|
195
|
+
exclude_dirs: set,
|
|
196
|
+
max_results: int,
|
|
197
|
+
case_sensitive: bool,
|
|
198
|
+
) -> list[str]:
|
|
199
|
+
"""Use CodeIndex for faster file matching."""
|
|
200
|
+
all_files = code_index.get_all_files()
|
|
201
|
+
matches = []
|
|
202
|
+
|
|
203
|
+
for file_path in all_files:
|
|
204
|
+
abs_path = code_index.root_dir / file_path
|
|
205
|
+
|
|
206
|
+
for pattern in patterns:
|
|
207
|
+
if _match_pattern(str(file_path), pattern, case_sensitive):
|
|
208
|
+
if not any(d in file_path.parts for d in exclude_dirs):
|
|
209
|
+
matches.append(str(abs_path))
|
|
210
|
+
if len(matches) >= max_results:
|
|
211
|
+
return matches
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
return matches
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _match_pattern(path: str, pattern: str, case_sensitive: bool) -> bool:
|
|
218
|
+
"""Match a path against a glob pattern."""
|
|
219
|
+
if "**" in pattern:
|
|
220
|
+
if pattern.startswith("**/"):
|
|
221
|
+
suffix = pattern[3:]
|
|
222
|
+
if case_sensitive:
|
|
223
|
+
if fnmatch.fnmatch(path, suffix):
|
|
224
|
+
return True
|
|
225
|
+
else:
|
|
226
|
+
if fnmatch.fnmatch(path.lower(), suffix.lower()):
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
regex_pat = pattern.replace("**", "__STARSTAR__")
|
|
230
|
+
regex_pat = fnmatch.translate(regex_pat)
|
|
231
|
+
regex_pat = regex_pat.replace("__STARSTAR__", ".*")
|
|
232
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
233
|
+
return bool(re.match(regex_pat, path, flags))
|
|
234
|
+
else:
|
|
235
|
+
if case_sensitive:
|
|
236
|
+
return fnmatch.fnmatch(path, pattern)
|
|
237
|
+
return fnmatch.fnmatch(path.lower(), pattern.lower())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def _glob_filesystem(
|
|
241
|
+
root: Path,
|
|
242
|
+
patterns: list[str],
|
|
243
|
+
recursive: bool,
|
|
244
|
+
include_hidden: bool,
|
|
245
|
+
exclude_dirs: set,
|
|
246
|
+
max_results: int,
|
|
247
|
+
case_sensitive: bool,
|
|
248
|
+
) -> list[str]:
|
|
249
|
+
"""Perform glob search using os.scandir."""
|
|
250
|
+
|
|
251
|
+
def search_sync():
|
|
252
|
+
matches = []
|
|
253
|
+
stack = [root]
|
|
254
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
255
|
+
|
|
256
|
+
compiled = []
|
|
257
|
+
for pat in patterns:
|
|
258
|
+
if "**" in pat:
|
|
259
|
+
regex_pat = pat.replace("**", "__STARSTAR__")
|
|
260
|
+
regex_pat = fnmatch.translate(regex_pat)
|
|
261
|
+
regex_pat = regex_pat.replace("__STARSTAR__", ".*")
|
|
262
|
+
compiled.append((pat, re.compile(regex_pat, flags)))
|
|
263
|
+
else:
|
|
264
|
+
compiled.append((pat, re.compile(fnmatch.translate(pat), flags)))
|
|
265
|
+
|
|
266
|
+
while stack and len(matches) < max_results:
|
|
267
|
+
current = stack.pop()
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
with os.scandir(current) as entries:
|
|
271
|
+
for entry in entries:
|
|
272
|
+
if not include_hidden and entry.name.startswith("."):
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
if entry.is_dir(follow_symlinks=False):
|
|
276
|
+
if entry.name not in exclude_dirs and recursive:
|
|
277
|
+
stack.append(Path(entry.path))
|
|
278
|
+
elif entry.is_file(follow_symlinks=False):
|
|
279
|
+
rel_path = os.path.relpath(entry.path, root)
|
|
280
|
+
|
|
281
|
+
for orig, comp in compiled:
|
|
282
|
+
if "**" in orig:
|
|
283
|
+
if orig.startswith("**/") and not recursive:
|
|
284
|
+
suffix = orig[3:]
|
|
285
|
+
if fnmatch.fnmatch(entry.name, suffix):
|
|
286
|
+
matches.append(entry.path)
|
|
287
|
+
break
|
|
288
|
+
elif comp.match(rel_path):
|
|
289
|
+
matches.append(entry.path)
|
|
290
|
+
break
|
|
291
|
+
elif orig.startswith("**/"):
|
|
292
|
+
suffix = orig[3:]
|
|
293
|
+
if fnmatch.fnmatch(entry.name, suffix):
|
|
294
|
+
matches.append(entry.path)
|
|
295
|
+
break
|
|
296
|
+
else:
|
|
297
|
+
if comp.match(entry.name):
|
|
298
|
+
matches.append(entry.path)
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
if len(matches) >= max_results:
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
except (PermissionError, OSError):
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
return matches[:max_results]
|
|
308
|
+
|
|
309
|
+
return await asyncio.get_event_loop().run_in_executor(None, search_sync)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def _sort_matches(matches: list[str], sort_by: SortOrder) -> list[str]:
|
|
313
|
+
"""Sort matches based on the specified order."""
|
|
314
|
+
if not matches:
|
|
315
|
+
return matches
|
|
316
|
+
|
|
317
|
+
def sort_sync():
|
|
318
|
+
if sort_by == SortOrder.MODIFIED:
|
|
319
|
+
return sorted(matches, key=lambda p: os.path.getmtime(p), reverse=True)
|
|
320
|
+
elif sort_by == SortOrder.SIZE:
|
|
321
|
+
return sorted(matches, key=lambda p: os.path.getsize(p), reverse=True)
|
|
322
|
+
elif sort_by == SortOrder.DEPTH:
|
|
323
|
+
return sorted(matches, key=lambda p: (p.count(os.sep), p))
|
|
324
|
+
return sorted(matches)
|
|
325
|
+
|
|
326
|
+
return await asyncio.get_event_loop().run_in_executor(None, sort_sync)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _format_output(pattern: str, matches: list[str], max_results: int, source: str) -> str:
|
|
330
|
+
"""Format glob results with source marker and header for rich panel parsing.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
pattern: The glob pattern used.
|
|
334
|
+
matches: List of matching file paths.
|
|
335
|
+
max_results: Maximum results limit.
|
|
336
|
+
source: "index" or "filesystem" to indicate cache hit/miss.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Formatted output with source marker and file count header.
|
|
340
|
+
"""
|
|
341
|
+
parts = [f"[source:{source}]"]
|
|
342
|
+
file_count = len(matches)
|
|
343
|
+
file_word = "file" if file_count == 1 else "files"
|
|
344
|
+
parts.append(f"Found {file_count} {file_word} matching pattern: {pattern}")
|
|
345
|
+
|
|
346
|
+
if matches:
|
|
347
|
+
parts.append("") # Blank line
|
|
348
|
+
parts.extend(matches)
|
|
349
|
+
|
|
350
|
+
if file_count == max_results:
|
|
351
|
+
parts.append(f"(truncated at {max_results})")
|
|
352
|
+
|
|
353
|
+
return "\n".join(parts)
|