janito 1.10.0__py3-none-any.whl → 1.11.0__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.
- janito/__init__.py +1 -1
- janito/agent/conversation_api.py +178 -90
- janito/agent/conversation_ui.py +1 -1
- janito/agent/llm_conversation_history.py +12 -0
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +19 -4
- janito/agent/tools/__init__.py +2 -0
- janito/agent/tools/create_directory.py +1 -1
- janito/agent/tools/create_file.py +1 -1
- janito/agent/tools/fetch_url.py +1 -1
- janito/agent/tools/find_files.py +26 -13
- janito/agent/tools/get_file_outline/core.py +1 -1
- janito/agent/tools/get_file_outline/python_outline.py +139 -95
- janito/agent/tools/get_lines.py +92 -63
- janito/agent/tools/move_file.py +58 -32
- janito/agent/tools/open_url.py +31 -0
- janito/agent/tools/python_command_runner.py +85 -86
- janito/agent/tools/python_file_runner.py +85 -86
- janito/agent/tools/python_stdin_runner.py +87 -88
- janito/agent/tools/remove_directory.py +1 -1
- janito/agent/tools/remove_file.py +1 -1
- janito/agent/tools/replace_file.py +2 -2
- janito/agent/tools/replace_text_in_file.py +193 -149
- janito/agent/tools/run_bash_command.py +1 -1
- janito/agent/tools/run_powershell_command.py +4 -0
- janito/agent/tools/search_text/__init__.py +1 -0
- janito/agent/tools/search_text/core.py +176 -0
- janito/agent/tools/search_text/match_lines.py +58 -0
- janito/agent/tools/search_text/pattern_utils.py +65 -0
- janito/agent/tools/search_text/traverse_directory.py +127 -0
- janito/agent/tools/validate_file_syntax/core.py +41 -30
- janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
- janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
- janito/agent/tools_utils/gitignore_utils.py +25 -2
- janito/agent/tools_utils/utils.py +7 -1
- janito/cli/config_commands.py +112 -109
- janito/shell/main.py +51 -8
- janito/shell/session/config.py +83 -75
- janito/shell/ui/interactive.py +97 -73
- janito/termweb/static/editor.css +32 -29
- janito/termweb/static/editor.css.bak +140 -22
- janito/termweb/static/editor.html +12 -7
- janito/termweb/static/editor.html.bak +16 -11
- janito/termweb/static/editor.js +94 -40
- janito/termweb/static/editor.js.bak +97 -65
- janito/termweb/static/index.html +1 -2
- janito/termweb/static/index.html.bak +1 -1
- janito/termweb/static/termweb.css +1 -22
- janito/termweb/static/termweb.css.bak +6 -4
- janito/termweb/static/termweb.js +0 -6
- janito/termweb/static/termweb.js.bak +1 -2
- {janito-1.10.0.dist-info → janito-1.11.0.dist-info}/METADATA +1 -1
- {janito-1.10.0.dist-info → janito-1.11.0.dist-info}/RECORD +56 -51
- {janito-1.10.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/search_text.py +0 -254
- {janito-1.10.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
- {janito-1.10.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.10.0.dist-info → janito-1.11.0.dist-info}/top_level.txt +0 -0
janito/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "1.
|
1
|
+
__version__ = "1.11.0"
|
janito/agent/conversation_api.py
CHANGED
@@ -9,19 +9,19 @@ from janito.agent.runtime_config import runtime_config
|
|
9
9
|
from janito.agent.tool_registry import get_tool_schemas
|
10
10
|
from janito.agent.conversation_exceptions import NoToolSupportError, EmptyResponseError
|
11
11
|
from janito.agent.api_exceptions import ApiError
|
12
|
+
from rich.console import Console
|
13
|
+
from rich.status import Status
|
14
|
+
|
15
|
+
console = Console()
|
12
16
|
|
13
17
|
|
14
18
|
def _sanitize_utf8_surrogates(obj):
|
15
|
-
|
16
|
-
Recursively sanitize a dict/list/string by replacing surrogate codepoints with the unicode replacement character.
|
17
|
-
"""
|
18
|
-
if isinstance(obj, str):
|
19
|
-
# Encode with surrogatepass, then decode with 'utf-8', replacing errors
|
20
|
-
return obj.encode("utf-8", "replace").decode("utf-8", "replace")
|
21
|
-
elif isinstance(obj, dict):
|
19
|
+
if isinstance(obj, dict):
|
22
20
|
return {k: _sanitize_utf8_surrogates(v) for k, v in obj.items()}
|
23
21
|
elif isinstance(obj, list):
|
24
|
-
return [_sanitize_utf8_surrogates(
|
22
|
+
return [_sanitize_utf8_surrogates(i) for i in obj]
|
23
|
+
elif isinstance(obj, str):
|
24
|
+
return obj.encode("utf-8", "surrogatepass").decode("utf-8", "ignore")
|
25
25
|
else:
|
26
26
|
return obj
|
27
27
|
|
@@ -75,110 +75,176 @@ def _extract_status_and_retry_after(e, error_message):
|
|
75
75
|
status_code = getattr(e, "status_code")
|
76
76
|
elif hasattr(e, "response") and hasattr(e.response, "status_code"):
|
77
77
|
status_code = getattr(e.response, "status_code")
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
retry_after = retry_after_match.group(1)
|
78
|
+
elif "429" in error_message:
|
79
|
+
status_code = 429
|
80
|
+
import re
|
81
|
+
|
82
|
+
match = re.search(r"status[ _]?code[=: ]+([0-9]+)", error_message)
|
83
|
+
if match:
|
84
|
+
status_code = int(match.group(1))
|
85
|
+
match_retry = re.search(r"retry[-_ ]?after[=: ]+([0-9]+)", error_message)
|
86
|
+
if match_retry:
|
87
|
+
retry_after = int(match_retry.group(1))
|
89
88
|
return status_code, retry_after
|
90
89
|
|
91
90
|
|
92
91
|
def _calculate_wait_time(status_code, retry_after, attempt):
|
93
|
-
if status_code == 429 and retry_after
|
94
|
-
|
95
|
-
return int(float(retry_after))
|
96
|
-
except Exception:
|
97
|
-
return 2**attempt
|
92
|
+
if status_code == 429 and retry_after:
|
93
|
+
return max(retry_after, 2**attempt)
|
98
94
|
return 2**attempt
|
99
95
|
|
100
96
|
|
101
|
-
def _log_and_sleep(
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
97
|
+
def _log_and_sleep(
|
98
|
+
message,
|
99
|
+
attempt,
|
100
|
+
max_retries,
|
101
|
+
e=None,
|
102
|
+
wait_time=None,
|
103
|
+
status=None,
|
104
|
+
waiting_message=None,
|
105
|
+
restore_message=None,
|
106
|
+
):
|
107
|
+
status_message = tr(
|
108
|
+
message,
|
109
|
+
attempt=attempt,
|
110
|
+
max_retries=max_retries,
|
111
|
+
e=e,
|
112
|
+
wait_time=wait_time,
|
110
113
|
)
|
111
|
-
|
114
|
+
if (
|
115
|
+
status is not None
|
116
|
+
and waiting_message is not None
|
117
|
+
and restore_message is not None
|
118
|
+
):
|
119
|
+
original_message = status.status
|
120
|
+
status.update(waiting_message)
|
121
|
+
time.sleep(wait_time)
|
122
|
+
status.update(restore_message)
|
123
|
+
else:
|
124
|
+
with Status(status_message, console=console, spinner="dots"):
|
125
|
+
time.sleep(wait_time)
|
112
126
|
|
113
127
|
|
114
|
-
def _handle_json_decode_error(e, attempt, max_retries):
|
128
|
+
def _handle_json_decode_error(e, attempt, max_retries, status=None):
|
115
129
|
if attempt < max_retries:
|
116
130
|
wait_time = 2**attempt
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
131
|
+
if status is not None:
|
132
|
+
_log_and_sleep(
|
133
|
+
"Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds...",
|
134
|
+
attempt,
|
135
|
+
max_retries,
|
136
|
+
wait_time=wait_time,
|
137
|
+
status=status,
|
138
|
+
waiting_message="Waiting after error...",
|
139
|
+
restore_message="Waiting for AI response...",
|
140
|
+
)
|
141
|
+
else:
|
142
|
+
_log_and_sleep(
|
143
|
+
"Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds...",
|
144
|
+
attempt,
|
145
|
+
max_retries,
|
146
|
+
wait_time=wait_time,
|
147
|
+
)
|
123
148
|
return None
|
124
149
|
else:
|
125
150
|
print(tr("Max retries for invalid response reached. Raising error."))
|
126
151
|
raise e
|
127
152
|
|
128
153
|
|
129
|
-
def
|
130
|
-
error_message = str(e)
|
154
|
+
def _handle_no_tool_support(error_message):
|
131
155
|
if "No endpoints found that support tool use" in error_message:
|
132
156
|
print(tr("API does not support tool use."))
|
133
157
|
raise NoToolSupportError(error_message)
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
e=e,
|
158
|
-
wait_time=wait_time,
|
159
|
-
)
|
160
|
-
return None
|
161
|
-
else:
|
162
|
-
print("Max retries for OpenAI API server error reached. Raising error.")
|
163
|
-
raise e
|
164
|
-
elif 400 <= status_code < 500:
|
165
|
-
print(
|
166
|
-
tr(
|
167
|
-
"OpenAI API client error {status_code}: {e}. Not retrying.",
|
168
|
-
status_code=status_code,
|
169
|
-
e=e,
|
170
|
-
)
|
158
|
+
|
159
|
+
|
160
|
+
def _handle_rate_limit(e, attempt, max_retries, status, status_code, retry_after):
|
161
|
+
wait_time = _calculate_wait_time(status_code, retry_after, attempt)
|
162
|
+
if attempt < max_retries:
|
163
|
+
if status is not None:
|
164
|
+
_log_and_sleep(
|
165
|
+
"OpenAI API rate limit (429) (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
166
|
+
attempt,
|
167
|
+
max_retries,
|
168
|
+
e=e,
|
169
|
+
wait_time=wait_time,
|
170
|
+
status=status,
|
171
|
+
waiting_message="Waiting after rate limit reached...",
|
172
|
+
restore_message="Waiting for AI response...",
|
173
|
+
)
|
174
|
+
else:
|
175
|
+
_log_and_sleep(
|
176
|
+
"OpenAI API rate limit (429) (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
177
|
+
attempt,
|
178
|
+
max_retries,
|
179
|
+
e=e,
|
180
|
+
wait_time=wait_time,
|
171
181
|
)
|
172
|
-
|
182
|
+
return None
|
183
|
+
else:
|
184
|
+
raise e
|
185
|
+
|
186
|
+
|
187
|
+
def _handle_server_error(e, attempt, max_retries, status, status_code):
|
188
|
+
wait_time = 2**attempt
|
173
189
|
if attempt < max_retries:
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
190
|
+
if status is not None:
|
191
|
+
_log_and_sleep(
|
192
|
+
"OpenAI API server error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
193
|
+
attempt,
|
194
|
+
max_retries,
|
195
|
+
e=e,
|
196
|
+
wait_time=wait_time,
|
197
|
+
status=status,
|
198
|
+
waiting_message="Waiting after server error...",
|
199
|
+
restore_message="Waiting for AI response...",
|
200
|
+
)
|
201
|
+
else:
|
202
|
+
_log_and_sleep(
|
203
|
+
"OpenAI API server error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
204
|
+
attempt,
|
205
|
+
max_retries,
|
206
|
+
e=e,
|
207
|
+
wait_time=wait_time,
|
208
|
+
)
|
209
|
+
return None
|
210
|
+
else:
|
211
|
+
print("Max retries for OpenAI API server error reached. Raising error.")
|
212
|
+
raise e
|
213
|
+
|
214
|
+
|
215
|
+
def _handle_client_error(e, status_code):
|
216
|
+
print(
|
217
|
+
tr(
|
218
|
+
"OpenAI API client error {status_code}: {e}. Not retrying.",
|
219
|
+
status_code=status_code,
|
179
220
|
e=e,
|
180
|
-
wait_time=wait_time,
|
181
221
|
)
|
222
|
+
)
|
223
|
+
raise e
|
224
|
+
|
225
|
+
|
226
|
+
def _handle_generic_error(e, attempt, max_retries, status):
|
227
|
+
wait_time = 2**attempt
|
228
|
+
if attempt < max_retries:
|
229
|
+
if status is not None:
|
230
|
+
_log_and_sleep(
|
231
|
+
"OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
232
|
+
attempt,
|
233
|
+
max_retries,
|
234
|
+
e=e,
|
235
|
+
wait_time=wait_time,
|
236
|
+
status=status,
|
237
|
+
waiting_message="Waiting after error...",
|
238
|
+
restore_message="Waiting for AI response...",
|
239
|
+
)
|
240
|
+
else:
|
241
|
+
_log_and_sleep(
|
242
|
+
"OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
243
|
+
attempt,
|
244
|
+
max_retries,
|
245
|
+
e=e,
|
246
|
+
wait_time=wait_time,
|
247
|
+
)
|
182
248
|
print(f"[DEBUG] Exception repr: {repr(e)}")
|
183
249
|
return None
|
184
250
|
else:
|
@@ -186,8 +252,30 @@ def _handle_general_exception(e, attempt, max_retries):
|
|
186
252
|
raise e
|
187
253
|
|
188
254
|
|
255
|
+
def _handle_general_exception(e, attempt, max_retries, status=None):
|
256
|
+
error_message = str(e)
|
257
|
+
_handle_no_tool_support(error_message)
|
258
|
+
status_code, retry_after = _extract_status_and_retry_after(e, error_message)
|
259
|
+
if status_code is not None:
|
260
|
+
if status_code == 429:
|
261
|
+
return _handle_rate_limit(
|
262
|
+
e, attempt, max_retries, status, status_code, retry_after
|
263
|
+
)
|
264
|
+
elif 500 <= status_code < 600:
|
265
|
+
return _handle_server_error(e, attempt, max_retries, status, status_code)
|
266
|
+
elif 400 <= status_code < 500:
|
267
|
+
_handle_client_error(e, status_code)
|
268
|
+
return _handle_generic_error(e, attempt, max_retries, status)
|
269
|
+
|
270
|
+
|
189
271
|
def retry_api_call(
|
190
|
-
api_func,
|
272
|
+
api_func,
|
273
|
+
max_retries=5,
|
274
|
+
*args,
|
275
|
+
history=None,
|
276
|
+
user_message_on_empty=None,
|
277
|
+
status=None,
|
278
|
+
**kwargs,
|
191
279
|
):
|
192
280
|
for attempt in range(1, max_retries + 1):
|
193
281
|
try:
|
@@ -209,10 +297,10 @@ def retry_api_call(
|
|
209
297
|
else:
|
210
298
|
raise
|
211
299
|
except json.JSONDecodeError as e:
|
212
|
-
result = _handle_json_decode_error(e, attempt, max_retries)
|
300
|
+
result = _handle_json_decode_error(e, attempt, max_retries, status=status)
|
213
301
|
if result is not None:
|
214
302
|
return result
|
215
303
|
except Exception as e:
|
216
|
-
result = _handle_general_exception(e, attempt, max_retries)
|
304
|
+
result = _handle_general_exception(e, attempt, max_retries, status=status)
|
217
305
|
if result is not None:
|
218
306
|
return result
|
janito/agent/conversation_ui.py
CHANGED
@@ -8,7 +8,7 @@ from rich.console import Console
|
|
8
8
|
def show_spinner(message, func, *args, **kwargs):
|
9
9
|
console = Console()
|
10
10
|
with console.status(message, spinner="dots") as status:
|
11
|
-
result = func(*args, **kwargs)
|
11
|
+
result = func(*args, status=status, **kwargs)
|
12
12
|
status.stop()
|
13
13
|
return result
|
14
14
|
|
@@ -68,3 +68,15 @@ class LLMConversationHistory:
|
|
68
68
|
|
69
69
|
def __getitem__(self, idx):
|
70
70
|
return self._messages[idx]
|
71
|
+
|
72
|
+
def remove_last_message(self):
|
73
|
+
"""Remove and return the last message in the history, or None if empty."""
|
74
|
+
if self._messages:
|
75
|
+
return self._messages.pop()
|
76
|
+
return None
|
77
|
+
|
78
|
+
def last_message(self):
|
79
|
+
"""Return the last message in the history, or None if empty."""
|
80
|
+
if self._messages:
|
81
|
+
return self._messages[-1]
|
82
|
+
return None
|
@@ -1,15 +1,30 @@
|
|
1
|
+
{# General role setup
|
2
|
+
ex. "Search in code" -> Python Developer -> find(*.py) | Java Developer -> find(*.java)
|
3
|
+
#}
|
1
4
|
You are: {{ role }}
|
2
5
|
|
6
|
+
{# Improves tool selection and platform specific constrains, eg, path format, C:\ vs /path #}
|
3
7
|
You will be developing and testing in the following environment:
|
4
8
|
Platform: {{ platform }}
|
5
9
|
Python version: {{ python_version }}
|
6
10
|
Shell/Environment: {{ shell_info }}
|
7
11
|
|
8
12
|
Respond according to the following guidelines:
|
9
|
-
|
13
|
+
{# Exploratory hint #}
|
14
|
+
- Before answering to the user, explore the content related to the question
|
15
|
+
{# Define exploration order, prefers search/outline, reduces chunking roundtip #}
|
16
|
+
- When exploring full files content, provide empty range to read the entire files instead of chunked reads
|
17
|
+
{# Prefix tools with purpose for user awarnesses #}
|
10
18
|
- Before using your namespace functions, provide a concise explanation.
|
11
|
-
|
12
|
-
-
|
19
|
+
{# Reduce unrequest code verbosity overhead #}
|
20
|
+
- Use the namespace functions to deliver the code changes instead of showing the code.
|
21
|
+
{# Drive edit mode, place holders critical as shown to be crucial to avoid corruption with code placeholders #}
|
13
22
|
- Prefer making localized edits using string replacements. If the required change is extensive, replace the entire file instead, provide full content without placeholders.
|
23
|
+
{# Trying to prevent surrogates generation, found this frequently in gpt4.1/windows #}
|
24
|
+
- While writing code, if you need an emoji or special Unicode character in a string, then insert the actual character (e.g., 📖) directly instead of using surrogate pairs or escape sequences.
|
25
|
+
{# Without this, the LLM choses to create files from a literal interpretation of the purpose and intention #}
|
14
26
|
- Before creating files search the code for the location related to the file purpose
|
15
|
-
|
27
|
+
{# This will trigger a search for the old names/locations to be updates #}
|
28
|
+
- After moving, removing or renaming functions or classes to different modules, update all imports, references, tests, and documentation to reflect the new locations, then verify functionality.
|
29
|
+
{# Keeping docstrings update is key to have semanatic match between prompts and code #}
|
30
|
+
- Once development or updates are finished, ensure that new or updated packages, modules, functions are properly documented.
|
janito/agent/tools/__init__.py
CHANGED
@@ -3,6 +3,7 @@ from . import create_directory
|
|
3
3
|
from . import create_file
|
4
4
|
from . import replace_file
|
5
5
|
from . import fetch_url
|
6
|
+
from . import open_url
|
6
7
|
from . import find_files
|
7
8
|
from . import get_lines
|
8
9
|
from .get_file_outline import core # noqa: F401,F811
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
25
26
|
"create_directory",
|
26
27
|
"create_file",
|
27
28
|
"fetch_url",
|
29
|
+
"open_url",
|
28
30
|
"find_files",
|
29
31
|
"GetFileOutlineTool",
|
30
32
|
"get_lines",
|
@@ -26,7 +26,7 @@ class CreateDirectoryTool(ToolBase):
|
|
26
26
|
disp_path = display_path(file_path)
|
27
27
|
self.report_info(
|
28
28
|
ActionType.WRITE,
|
29
|
-
tr("📁
|
29
|
+
tr("📁 Create directory '{disp_path}' ...", disp_path=disp_path),
|
30
30
|
)
|
31
31
|
try:
|
32
32
|
if os.path.exists(file_path):
|
@@ -45,7 +45,7 @@ class CreateFileTool(ToolBase):
|
|
45
45
|
os.makedirs(dir_name, exist_ok=True)
|
46
46
|
self.report_info(
|
47
47
|
ActionType.WRITE,
|
48
|
-
tr("📝
|
48
|
+
tr("📝 Create file '{disp_path}' ...", disp_path=disp_path),
|
49
49
|
)
|
50
50
|
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
51
51
|
f.write(content)
|
janito/agent/tools/fetch_url.py
CHANGED
@@ -25,7 +25,7 @@ class FetchUrlTool(ToolBase):
|
|
25
25
|
if not url.strip():
|
26
26
|
self.report_warning(tr("ℹ️ Empty URL provided."))
|
27
27
|
return tr("Warning: Empty URL provided. Operation skipped.")
|
28
|
-
self.report_info(ActionType.READ, tr("🌐
|
28
|
+
self.report_info(ActionType.READ, tr("🌐 Fetch URL '{url}' ...", url=url))
|
29
29
|
try:
|
30
30
|
response = requests.get(url, timeout=10)
|
31
31
|
response.raise_for_status()
|
janito/agent/tools/find_files.py
CHANGED
@@ -25,6 +25,26 @@ class FindFilesTool(ToolBase):
|
|
25
25
|
If max_results is reached, appends a note to the output.
|
26
26
|
"""
|
27
27
|
|
28
|
+
def _match_directories(self, root, dirs, pat):
|
29
|
+
dir_output = set()
|
30
|
+
dir_pat = pat.rstrip("/\\")
|
31
|
+
for d in dirs:
|
32
|
+
if fnmatch.fnmatch(d, dir_pat):
|
33
|
+
dir_output.add(os.path.join(root, d) + os.sep)
|
34
|
+
return dir_output
|
35
|
+
|
36
|
+
def _match_files(self, root, files, pat):
|
37
|
+
file_output = set()
|
38
|
+
for filename in fnmatch.filter(files, pat):
|
39
|
+
file_output.add(os.path.join(root, filename))
|
40
|
+
return file_output
|
41
|
+
|
42
|
+
def _match_dirs_without_slash(self, root, dirs, pat):
|
43
|
+
dir_output = set()
|
44
|
+
for d in fnmatch.filter(dirs, pat):
|
45
|
+
dir_output.add(os.path.join(root, d))
|
46
|
+
return dir_output
|
47
|
+
|
28
48
|
def run(self, paths: str, pattern: str, max_depth: int = None) -> str:
|
29
49
|
if not pattern:
|
30
50
|
self.report_warning(tr("ℹ️ Empty file pattern provided."))
|
@@ -41,7 +61,7 @@ class FindFilesTool(ToolBase):
|
|
41
61
|
self.report_info(
|
42
62
|
ActionType.READ,
|
43
63
|
tr(
|
44
|
-
"🔍
|
64
|
+
"🔍 Search for files '{pattern}' in '{disp_path}'{depth_msg} ...",
|
45
65
|
pattern=pattern,
|
46
66
|
disp_path=disp_path,
|
47
67
|
depth_msg=depth_msg,
|
@@ -52,19 +72,13 @@ class FindFilesTool(ToolBase):
|
|
52
72
|
directory, max_depth=max_depth
|
53
73
|
):
|
54
74
|
for pat in patterns:
|
55
|
-
# Directory matching: pattern ends with '/' or '\'
|
56
75
|
if pat.endswith("/") or pat.endswith("\\"):
|
57
|
-
|
58
|
-
for d in dirs:
|
59
|
-
if fnmatch.fnmatch(d, dir_pat):
|
60
|
-
dir_output.add(os.path.join(root, d) + os.sep)
|
76
|
+
dir_output.update(self._match_directories(root, dirs, pat))
|
61
77
|
else:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
for d in fnmatch.filter(dirs, pat):
|
67
|
-
dir_output.add(os.path.join(root, d))
|
78
|
+
dir_output.update(self._match_files(root, files, pat))
|
79
|
+
dir_output.update(
|
80
|
+
self._match_dirs_without_slash(root, dirs, pat)
|
81
|
+
)
|
68
82
|
self.report_success(
|
69
83
|
tr(
|
70
84
|
" ✅ {count} {file_word}",
|
@@ -72,7 +86,6 @@ class FindFilesTool(ToolBase):
|
|
72
86
|
file_word=pluralize("file", len(dir_output)),
|
73
87
|
)
|
74
88
|
)
|
75
|
-
# If searching in '.', strip leading './' from results
|
76
89
|
if directory.strip() == ".":
|
77
90
|
dir_output = {
|
78
91
|
p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
|