janito 1.8.1__py3-none-any.whl → 1.10.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/api_exceptions.py +4 -0
- janito/agent/config.py +1 -1
- janito/agent/config_defaults.py +2 -3
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +177 -114
- janito/agent/conversation_api.py +179 -159
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/llm_conversation_history.py +70 -0
- janito/agent/openai_client.py +44 -21
- janito/agent/openai_schema_generator.py +164 -128
- janito/agent/platform_discovery.py +134 -77
- janito/agent/profile_manager.py +5 -5
- janito/agent/rich_message_handler.py +80 -31
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
- janito/agent/test_openai_schema_generator.py +93 -0
- janito/agent/tool_base.py +7 -2
- janito/agent/tool_executor.py +63 -50
- janito/agent/tool_registry.py +5 -2
- janito/agent/tool_use_tracker.py +42 -5
- janito/agent/tools/__init__.py +13 -12
- janito/agent/tools/create_directory.py +9 -6
- janito/agent/tools/create_file.py +35 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +50 -5
- janito/agent/tools/find_files.py +40 -26
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
- janito/agent/tools/get_lines.py +21 -12
- janito/agent/tools/move_file.py +13 -12
- janito/agent/tools/present_choices.py +3 -1
- janito/agent/tools/python_command_runner.py +150 -0
- janito/agent/tools/python_file_runner.py +148 -0
- janito/agent/tools/python_stdin_runner.py +154 -0
- janito/agent/tools/remove_directory.py +4 -2
- janito/agent/tools/remove_file.py +15 -13
- janito/agent/tools/replace_file.py +72 -0
- janito/agent/tools/replace_text_in_file.py +7 -5
- janito/agent/tools/run_bash_command.py +29 -72
- janito/agent/tools/run_powershell_command.py +142 -102
- janito/agent/tools/search_text.py +177 -131
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +94 -0
- janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
- janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
- janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
- janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
- janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
- janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
- janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
- janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
- janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
- janito/agent/tools_utils/__init__.py +1 -0
- janito/agent/tools_utils/action_type.py +7 -0
- janito/agent/tools_utils/dir_walk_utils.py +24 -0
- janito/agent/tools_utils/formatting.py +49 -0
- janito/agent/tools_utils/gitignore_utils.py +69 -0
- janito/agent/tools_utils/test_gitignore_utils.py +46 -0
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/_print_config.py +63 -61
- janito/cli/arg_parser.py +57 -14
- janito/cli/cli_main.py +270 -0
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +166 -99
- janito/cli/one_shot.py +80 -0
- janito/cli/termweb_starter.py +2 -2
- janito/i18n/__init__.py +1 -1
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +41 -25
- janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +74 -0
- janito/shell/commands/edit.py +24 -0
- janito/shell/commands/history_view.py +18 -0
- janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
- janito/shell/commands/livelogs.py +42 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
- janito/shell/commands/session.py +35 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
- janito/shell/commands/tools.py +26 -0
- janito/shell/commands/track.py +36 -0
- janito/shell/commands/utility.py +28 -0
- janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
- janito/shell/commands.py +40 -0
- janito/shell/input_history.py +62 -0
- janito/shell/main.py +257 -0
- janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
- janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
- janito/shell/session/manager.py +101 -0
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +142 -0
- janito/termweb/static/editor.css.bak +27 -0
- janito/termweb/static/editor.html +15 -213
- janito/termweb/static/editor.html.bak +16 -215
- janito/termweb/static/editor.js +209 -0
- janito/termweb/static/editor.js.bak +227 -0
- janito/termweb/static/index.html +2 -3
- janito/termweb/static/index.html.bak +2 -3
- janito/termweb/static/termweb.css.bak +33 -84
- janito/termweb/static/termweb.js +15 -34
- janito/termweb/static/termweb.js.bak +18 -36
- janito/tests/test_rich_utils.py +44 -0
- janito/web/app.py +0 -75
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
- janito-1.10.0.dist-info/RECORD +158 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/gitignore_utils.py +0 -46
- janito/agent/tools/memory.py +0 -48
- janito/agent/tools/outline_file/formatting.py +0 -20
- janito/agent/tools/outline_file/python_outline.py +0 -71
- janito/agent/tools/present_choices_test.py +0 -18
- janito/agent/tools/rich_live.py +0 -44
- janito/agent/tools/run_python_command.py +0 -163
- janito/agent/tools/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- janito/cli/runner/cli_main.py +0 -180
- janito/cli_chat_shell/chat_loop.py +0 -163
- janito/cli_chat_shell/chat_state.py +0 -38
- janito/cli_chat_shell/commands/history_start.py +0 -37
- janito/cli_chat_shell/commands/session.py +0 -48
- janito/cli_chat_shell/commands/sum.py +0 -49
- janito/cli_chat_shell/commands/utility.py +0 -32
- janito/cli_chat_shell/session_manager.py +0 -72
- janito-1.8.1.dist-info/RECORD +0 -127
- /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
- /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
- /janito/cli/{runner/config.py → config_runner.py} +0 -0
- /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
- /janito/{cli/runner → shell}/__init__.py +0 -0
- /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
- /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
- /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,11 @@
|
|
1
1
|
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tools_utils.action_type import ActionType
|
2
3
|
from janito.agent.tool_registry import register_tool
|
3
|
-
from janito.agent.
|
4
|
+
from janito.agent.tools_utils.utils import pluralize
|
4
5
|
from janito.i18n import tr
|
5
6
|
import os
|
6
7
|
import re
|
7
|
-
from janito.agent.
|
8
|
+
from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
|
8
9
|
|
9
10
|
|
10
11
|
def is_binary_file(path, blocksize=1024):
|
@@ -28,11 +29,13 @@ def is_binary_file(path, blocksize=1024):
|
|
28
29
|
class SearchTextTool(ToolBase):
|
29
30
|
"""
|
30
31
|
Search for a text pattern (regex or plain string) in all files within one or more directories or file paths and return matching lines. Respects .gitignore.
|
31
|
-
|
32
32
|
Args:
|
33
33
|
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory or a file.
|
34
|
-
pattern (str): Regex pattern or plain text substring to search for in files. Tries regex first, falls back to substring if regex is invalid.
|
35
|
-
|
34
|
+
pattern (str): Regex pattern or plain text substring to search for in files. Must not be empty. Tries regex first, falls back to substring if regex is invalid.
|
35
|
+
Note: When using regex mode, special characters (such as [, ], ., *, etc.) must be escaped if you want to match them literally (e.g., use '\\[DEBUG\\]' to match the literal string '[DEBUG]').
|
36
|
+
is_regex (bool): If True, treat pattern as a regular expression. If False, treat as plain text (default).
|
37
|
+
Only set is_regex=True if your pattern is a valid regular expression. Do NOT set is_regex=True for plain text patterns, as regex special characters (such as ., *, [, ], etc.) will be interpreted and may cause unexpected results.
|
38
|
+
For plain text substring search, leave is_regex as False or omit it.
|
36
39
|
max_depth (int, optional): Maximum directory depth to search. If 0 (default), search is recursive with no depth limit. If >0, limits recursion to that depth. Setting max_depth=1 disables recursion (only top-level directory). Ignored for file paths.
|
37
40
|
max_results (int): Maximum number of results to return. 0 means no limit (default).
|
38
41
|
ignore_utf8_errors (bool): If True, ignore utf-8 decode errors. Defaults to True.
|
@@ -41,20 +44,16 @@ class SearchTextTool(ToolBase):
|
|
41
44
|
If max_results is reached, appends a note to the output.
|
42
45
|
"""
|
43
46
|
|
44
|
-
def
|
45
|
-
self,
|
46
|
-
paths: str,
|
47
|
-
pattern: str,
|
48
|
-
is_regex: bool = False,
|
49
|
-
max_depth: int = 0,
|
50
|
-
max_results: int = 0,
|
51
|
-
ignore_utf8_errors: bool = True,
|
52
|
-
) -> str:
|
47
|
+
def _prepare_pattern(self, pattern, is_regex):
|
53
48
|
if not pattern:
|
54
|
-
self.
|
55
|
-
tr("
|
49
|
+
self.report_error(
|
50
|
+
tr("Error: Empty search pattern provided. Operation aborted.")
|
51
|
+
)
|
52
|
+
return (
|
53
|
+
None,
|
54
|
+
False,
|
55
|
+
tr("Error: Empty search pattern provided. Operation aborted."),
|
56
56
|
)
|
57
|
-
return tr("Warning: Empty search pattern provided. Operation skipped.")
|
58
57
|
regex = None
|
59
58
|
use_regex = False
|
60
59
|
if is_regex:
|
@@ -62,14 +61,11 @@ class SearchTextTool(ToolBase):
|
|
62
61
|
regex = re.compile(pattern)
|
63
62
|
use_regex = True
|
64
63
|
except re.error as e:
|
65
|
-
self.report_warning(
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
)
|
70
|
-
)
|
71
|
-
return tr(
|
72
|
-
"Warning: Invalid regex pattern: {error}. No results.", error=e
|
64
|
+
self.report_warning(tr("\u26a0\ufe0f Invalid regex pattern."))
|
65
|
+
return (
|
66
|
+
None,
|
67
|
+
False,
|
68
|
+
tr("Warning: Invalid regex pattern: {error}. No results.", error=e),
|
73
69
|
)
|
74
70
|
else:
|
75
71
|
try:
|
@@ -78,116 +74,100 @@ class SearchTextTool(ToolBase):
|
|
78
74
|
except re.error:
|
79
75
|
regex = None
|
80
76
|
use_regex = False
|
81
|
-
|
82
|
-
limit_reached = False
|
83
|
-
total_results = 0
|
84
|
-
paths_list = paths.split()
|
85
|
-
for search_path in paths_list:
|
86
|
-
from janito.agent.tools.tools_utils import display_path
|
77
|
+
return regex, use_regex, None
|
87
78
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
dirs, files = filter_ignored(search_path, dirs, files)
|
141
|
-
walker = [(search_path, dirs, files)]
|
79
|
+
def _search_file(
|
80
|
+
self,
|
81
|
+
path,
|
82
|
+
pattern,
|
83
|
+
regex,
|
84
|
+
use_regex,
|
85
|
+
max_results,
|
86
|
+
total_results,
|
87
|
+
ignore_utf8_errors,
|
88
|
+
):
|
89
|
+
dir_output = []
|
90
|
+
dir_limit_reached = False
|
91
|
+
if not is_binary_file(path):
|
92
|
+
try:
|
93
|
+
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
94
|
+
if ignore_utf8_errors:
|
95
|
+
open_kwargs["errors"] = "ignore"
|
96
|
+
with open(path, **open_kwargs) as f:
|
97
|
+
for lineno, line in enumerate(f, 1):
|
98
|
+
if use_regex:
|
99
|
+
if regex.search(line):
|
100
|
+
dir_output.append(f"{path}:{lineno}: {line.strip()}")
|
101
|
+
else:
|
102
|
+
if pattern in line:
|
103
|
+
dir_output.append(f"{path}:{lineno}: {line.strip()}")
|
104
|
+
if (
|
105
|
+
max_results > 0
|
106
|
+
and (total_results + len(dir_output)) >= max_results
|
107
|
+
):
|
108
|
+
dir_limit_reached = True
|
109
|
+
break
|
110
|
+
except Exception:
|
111
|
+
pass
|
112
|
+
return dir_output, dir_limit_reached
|
113
|
+
|
114
|
+
def _search_directory(
|
115
|
+
self,
|
116
|
+
search_path,
|
117
|
+
pattern,
|
118
|
+
regex,
|
119
|
+
use_regex,
|
120
|
+
max_depth,
|
121
|
+
max_results,
|
122
|
+
total_results,
|
123
|
+
ignore_utf8_errors,
|
124
|
+
):
|
125
|
+
dir_output = []
|
126
|
+
dir_limit_reached = False
|
127
|
+
if max_depth == 1:
|
128
|
+
walk_result = next(os.walk(search_path), None)
|
129
|
+
if walk_result is None:
|
130
|
+
walker = [(search_path, [], [])]
|
142
131
|
else:
|
143
|
-
|
144
|
-
|
145
|
-
|
132
|
+
_, dirs, files = walk_result
|
133
|
+
gitignore = GitignoreFilter()
|
134
|
+
dirs, files = gitignore.filter_ignored(search_path, dirs, files)
|
135
|
+
walker = [(search_path, dirs, files)]
|
136
|
+
else:
|
137
|
+
gitignore = GitignoreFilter()
|
138
|
+
walker = os.walk(search_path)
|
139
|
+
stop_search = False
|
140
|
+
for root, dirs, files in walker:
|
141
|
+
if stop_search:
|
142
|
+
break
|
143
|
+
rel_path = os.path.relpath(root, search_path)
|
144
|
+
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
145
|
+
if max_depth == 1 and depth > 0:
|
146
|
+
break
|
147
|
+
if max_depth > 0 and depth > max_depth:
|
148
|
+
continue
|
149
|
+
dirs, files = gitignore.filter_ignored(root, dirs, files)
|
150
|
+
for filename in files:
|
146
151
|
if stop_search:
|
147
152
|
break
|
148
|
-
|
149
|
-
|
150
|
-
|
153
|
+
path = os.path.join(root, filename)
|
154
|
+
file_output, file_limit_reached = self._search_file(
|
155
|
+
path,
|
156
|
+
pattern,
|
157
|
+
regex,
|
158
|
+
use_regex,
|
159
|
+
max_results,
|
160
|
+
total_results + len(dir_output),
|
161
|
+
ignore_utf8_errors,
|
162
|
+
)
|
163
|
+
dir_output.extend(file_output)
|
164
|
+
if file_limit_reached:
|
165
|
+
dir_limit_reached = True
|
166
|
+
stop_search = True
|
151
167
|
break
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
for filename in files:
|
156
|
-
if stop_search:
|
157
|
-
break
|
158
|
-
path = os.path.join(root, filename)
|
159
|
-
if is_binary_file(path):
|
160
|
-
continue
|
161
|
-
try:
|
162
|
-
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
163
|
-
if ignore_utf8_errors:
|
164
|
-
open_kwargs["errors"] = "ignore"
|
165
|
-
with open(path, **open_kwargs) as f:
|
166
|
-
for lineno, line in enumerate(f, 1):
|
167
|
-
if use_regex:
|
168
|
-
if regex.search(line):
|
169
|
-
dir_output.append(
|
170
|
-
f"{path}:{lineno}: {line.strip()}"
|
171
|
-
)
|
172
|
-
else:
|
173
|
-
if pattern in line:
|
174
|
-
dir_output.append(
|
175
|
-
f"{path}:{lineno}: {line.strip()}"
|
176
|
-
)
|
177
|
-
if (
|
178
|
-
max_results > 0
|
179
|
-
and (total_results + len(dir_output)) >= max_results
|
180
|
-
):
|
181
|
-
dir_limit_reached = True
|
182
|
-
stop_search = True
|
183
|
-
break
|
184
|
-
except Exception:
|
185
|
-
continue
|
186
|
-
output.extend(dir_output)
|
187
|
-
total_results += len(dir_output)
|
188
|
-
if dir_limit_reached:
|
189
|
-
limit_reached = True
|
190
|
-
break
|
168
|
+
return dir_output, dir_limit_reached
|
169
|
+
|
170
|
+
def _format_result(self, pattern, use_regex, output, limit_reached):
|
191
171
|
header = tr(
|
192
172
|
"[search_text] Pattern: '{pattern}' | Regex: {use_regex} | Results: {count}",
|
193
173
|
pattern=pattern,
|
@@ -199,10 +179,76 @@ class SearchTextTool(ToolBase):
|
|
199
179
|
result += tr("\n[Note: max_results limit reached, output truncated.]")
|
200
180
|
self.report_success(
|
201
181
|
tr(
|
202
|
-
"
|
182
|
+
" \u2705 {count} {line_word}{limit}",
|
203
183
|
count=len(output),
|
204
184
|
line_word=pluralize("line", len(output)),
|
205
185
|
limit=(" (limit reached)" if limit_reached else ""),
|
206
186
|
)
|
207
187
|
)
|
208
188
|
return result
|
189
|
+
|
190
|
+
def run(
|
191
|
+
self,
|
192
|
+
paths: str,
|
193
|
+
pattern: str,
|
194
|
+
is_regex: bool = False,
|
195
|
+
max_depth: int = 0,
|
196
|
+
max_results: int = 0,
|
197
|
+
ignore_utf8_errors: bool = True,
|
198
|
+
) -> str:
|
199
|
+
regex, use_regex, error_msg = self._prepare_pattern(pattern, is_regex)
|
200
|
+
if error_msg:
|
201
|
+
return error_msg
|
202
|
+
paths_list = paths.split()
|
203
|
+
results = []
|
204
|
+
total_results = 0
|
205
|
+
limit_reached = False
|
206
|
+
for search_path in paths_list:
|
207
|
+
from janito.agent.tools_utils.utils import display_path
|
208
|
+
|
209
|
+
info_str = tr(
|
210
|
+
"\U0001f50d Searching for {search_type} '{pattern}' in '{disp_path}'",
|
211
|
+
search_type=("regex" if use_regex else "text"),
|
212
|
+
pattern=pattern,
|
213
|
+
disp_path=display_path(search_path),
|
214
|
+
)
|
215
|
+
if max_depth > 0:
|
216
|
+
info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
|
217
|
+
self.report_info(ActionType.READ, info_str)
|
218
|
+
dir_output = []
|
219
|
+
dir_limit_reached = False
|
220
|
+
if os.path.isfile(search_path):
|
221
|
+
dir_output, dir_limit_reached = self._search_file(
|
222
|
+
search_path,
|
223
|
+
pattern,
|
224
|
+
regex,
|
225
|
+
use_regex,
|
226
|
+
max_results,
|
227
|
+
total_results,
|
228
|
+
ignore_utf8_errors,
|
229
|
+
)
|
230
|
+
total_results += len(dir_output)
|
231
|
+
if dir_limit_reached:
|
232
|
+
limit_reached = True
|
233
|
+
else:
|
234
|
+
dir_output, dir_limit_reached = self._search_directory(
|
235
|
+
search_path,
|
236
|
+
pattern,
|
237
|
+
regex,
|
238
|
+
use_regex,
|
239
|
+
max_depth,
|
240
|
+
max_results,
|
241
|
+
total_results,
|
242
|
+
ignore_utf8_errors,
|
243
|
+
)
|
244
|
+
total_results += len(dir_output)
|
245
|
+
if dir_limit_reached:
|
246
|
+
limit_reached = True
|
247
|
+
# Format and append result for this path
|
248
|
+
result_str = self._format_result(
|
249
|
+
pattern, use_regex, dir_output, dir_limit_reached
|
250
|
+
)
|
251
|
+
results.append(info_str + "\n" + result_str)
|
252
|
+
if limit_reached:
|
253
|
+
break
|
254
|
+
return "\n\n".join(results)
|
@@ -0,0 +1 @@
|
|
1
|
+
# Validation syntax package
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import os
|
2
|
+
from janito.i18n import tr
|
3
|
+
from janito.agent.tool_base import ToolBase
|
4
|
+
from janito.agent.tools_utils.action_type import ActionType
|
5
|
+
from janito.agent.tool_registry import register_tool
|
6
|
+
from janito.agent.tools_utils.utils import display_path
|
7
|
+
|
8
|
+
from .python_validator import validate_python
|
9
|
+
from .json_validator import validate_json
|
10
|
+
from .yaml_validator import validate_yaml
|
11
|
+
from .ps1_validator import validate_ps1
|
12
|
+
from .xml_validator import validate_xml
|
13
|
+
from .html_validator import validate_html
|
14
|
+
from .markdown_validator import validate_markdown
|
15
|
+
from .js_validator import validate_js
|
16
|
+
from .css_validator import validate_css
|
17
|
+
|
18
|
+
|
19
|
+
def validate_file_syntax(
|
20
|
+
file_path: str, report_info=None, report_warning=None, report_success=None
|
21
|
+
) -> str:
|
22
|
+
ext = os.path.splitext(file_path)[1].lower()
|
23
|
+
try:
|
24
|
+
if ext in [".py", ".pyw"]:
|
25
|
+
return validate_python(file_path)
|
26
|
+
elif ext == ".json":
|
27
|
+
return validate_json(file_path)
|
28
|
+
elif ext in [".yml", ".yaml"]:
|
29
|
+
return validate_yaml(file_path)
|
30
|
+
elif ext == ".ps1":
|
31
|
+
return validate_ps1(file_path)
|
32
|
+
elif ext == ".xml":
|
33
|
+
return validate_xml(file_path)
|
34
|
+
elif ext in (".html", ".htm"):
|
35
|
+
return validate_html(file_path)
|
36
|
+
elif ext == ".md":
|
37
|
+
return validate_markdown(file_path)
|
38
|
+
elif ext == ".js":
|
39
|
+
return validate_js(file_path)
|
40
|
+
elif ext == ".css":
|
41
|
+
return validate_css(file_path)
|
42
|
+
else:
|
43
|
+
msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
|
44
|
+
if report_warning:
|
45
|
+
report_warning(msg)
|
46
|
+
return msg
|
47
|
+
except Exception as e:
|
48
|
+
msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
|
49
|
+
if report_warning:
|
50
|
+
report_warning(msg)
|
51
|
+
return msg
|
52
|
+
|
53
|
+
|
54
|
+
@register_tool(name="validate_file_syntax")
|
55
|
+
class ValidateFileSyntaxTool(ToolBase):
|
56
|
+
"""
|
57
|
+
Validate a file for syntax issues.
|
58
|
+
|
59
|
+
Supported types:
|
60
|
+
- Python (.py, .pyw)
|
61
|
+
- JSON (.json)
|
62
|
+
- YAML (.yml, .yaml)
|
63
|
+
- PowerShell (.ps1)
|
64
|
+
- XML (.xml)
|
65
|
+
- HTML (.html, .htm) [lxml]
|
66
|
+
- Markdown (.md)
|
67
|
+
- JavaScript (.js)
|
68
|
+
|
69
|
+
Args:
|
70
|
+
file_path (str): Path to the file to validate.
|
71
|
+
Returns:
|
72
|
+
str: Validation status message. Example:
|
73
|
+
- "✅ Syntax OK"
|
74
|
+
- "⚠️ Warning: Syntax error: <error message>"
|
75
|
+
- "⚠️ Warning: Unsupported file extension: <ext>"
|
76
|
+
"""
|
77
|
+
|
78
|
+
def run(self, file_path: str) -> str:
|
79
|
+
disp_path = display_path(file_path)
|
80
|
+
self.report_info(
|
81
|
+
ActionType.READ,
|
82
|
+
tr("🔎 Validating syntax for file '{disp_path}' ...", disp_path=disp_path),
|
83
|
+
)
|
84
|
+
result = validate_file_syntax(
|
85
|
+
file_path,
|
86
|
+
report_info=self.report_info,
|
87
|
+
report_warning=self.report_warning,
|
88
|
+
report_success=self.report_success,
|
89
|
+
)
|
90
|
+
if result.startswith("✅"):
|
91
|
+
self.report_success(result)
|
92
|
+
elif result.startswith("⚠️"):
|
93
|
+
self.report_warning(tr("⚠️ ") + result.lstrip("⚠️ "))
|
94
|
+
return result
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from janito.i18n import tr
|
2
|
+
import re
|
3
|
+
|
4
|
+
|
5
|
+
def validate_css(file_path: str) -> str:
|
6
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
7
|
+
content = f.read()
|
8
|
+
errors = []
|
9
|
+
# Check for unmatched curly braces
|
10
|
+
if content.count("{") != content.count("}"):
|
11
|
+
errors.append("Unmatched curly braces { }")
|
12
|
+
# Check for unclosed comments
|
13
|
+
if content.count("/*") != content.count("*/"):
|
14
|
+
errors.append("Unclosed comment (/* ... */)")
|
15
|
+
# Check for invalid property declarations (very basic)
|
16
|
+
for i, line in enumerate(content.splitlines(), 1):
|
17
|
+
# Ignore empty lines and comments
|
18
|
+
if not line.strip() or line.strip().startswith("/*"):
|
19
|
+
continue
|
20
|
+
# Match property: value; (allow whitespace)
|
21
|
+
if ":" in line and not re.search(r":.*;", line):
|
22
|
+
errors.append(
|
23
|
+
f"Line {i}: Missing semicolon after property value | {line.strip()}"
|
24
|
+
)
|
25
|
+
# Match lines with property but missing colon
|
26
|
+
if ";" in line and ":" not in line:
|
27
|
+
errors.append(
|
28
|
+
f"Line {i}: Missing colon in property declaration | {line.strip()}"
|
29
|
+
)
|
30
|
+
if errors:
|
31
|
+
msg = tr(
|
32
|
+
"⚠️ Warning: CSS syntax issues found:\n{errors}", errors="\n".join(errors)
|
33
|
+
)
|
34
|
+
return msg
|
35
|
+
return "✅ Syntax valid"
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from janito.i18n import tr
|
2
|
+
import re
|
3
|
+
from lxml import etree
|
4
|
+
|
5
|
+
|
6
|
+
def validate_html(file_path: str) -> str:
|
7
|
+
warnings = []
|
8
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
9
|
+
html_content = f.read()
|
10
|
+
script_blocks = [
|
11
|
+
m.span()
|
12
|
+
for m in re.finditer(
|
13
|
+
r"<script[\s\S]*?>[\s\S]*?<\/script>", html_content, re.IGNORECASE
|
14
|
+
)
|
15
|
+
]
|
16
|
+
js_patterns = [
|
17
|
+
r"document\.addEventListener",
|
18
|
+
r"^\s*(var|let|const)\s+\w+\s*[=;]",
|
19
|
+
r"^\s*function\s+\w+\s*\(",
|
20
|
+
r"^\s*(const|let|var)\s+\w+\s*=\s*\(.*\)\s*=>",
|
21
|
+
r"^\s*window\.\w+\s*=",
|
22
|
+
r"^\s*\$\s*\(",
|
23
|
+
]
|
24
|
+
for pat in js_patterns:
|
25
|
+
for m in re.finditer(pat, html_content):
|
26
|
+
in_script = False
|
27
|
+
for s_start, s_end in script_blocks:
|
28
|
+
if s_start <= m.start() < s_end:
|
29
|
+
in_script = True
|
30
|
+
break
|
31
|
+
if not in_script:
|
32
|
+
warnings.append(
|
33
|
+
f"Line {html_content.count(chr(10), 0, m.start())+1}: JavaScript code ('{pat}') found outside <script> tag."
|
34
|
+
)
|
35
|
+
lxml_error = None
|
36
|
+
try:
|
37
|
+
# Parse HTML and collect error log
|
38
|
+
parser = etree.HTMLParser(recover=False)
|
39
|
+
with open(file_path, "rb") as f:
|
40
|
+
etree.parse(f, parser=parser)
|
41
|
+
error_log = parser.error_log
|
42
|
+
# Look for tag mismatch or unclosed tag errors
|
43
|
+
syntax_errors = []
|
44
|
+
for e in error_log:
|
45
|
+
if (
|
46
|
+
"mismatch" in e.message.lower()
|
47
|
+
or "tag not closed" in e.message.lower()
|
48
|
+
or "unexpected end tag" in e.message.lower()
|
49
|
+
or "expected" in e.message.lower()
|
50
|
+
):
|
51
|
+
syntax_errors.append(str(e))
|
52
|
+
if syntax_errors:
|
53
|
+
lxml_error = tr("Syntax error: {error}", error="; ".join(syntax_errors))
|
54
|
+
elif error_log:
|
55
|
+
# Other warnings
|
56
|
+
lxml_error = tr(
|
57
|
+
"HTML syntax errors found:\n{errors}",
|
58
|
+
errors="\n".join(str(e) for e in error_log),
|
59
|
+
)
|
60
|
+
except ImportError:
|
61
|
+
lxml_error = tr("⚠️ lxml not installed. Cannot validate HTML.")
|
62
|
+
except Exception as e:
|
63
|
+
lxml_error = tr("Syntax error: {error}", error=str(e))
|
64
|
+
msg = ""
|
65
|
+
if warnings:
|
66
|
+
msg += (
|
67
|
+
tr(
|
68
|
+
"⚠️ Warning: JavaScript code found outside <script> tags. This is invalid HTML and will not execute in browsers.\n"
|
69
|
+
+ "\n".join(warnings)
|
70
|
+
)
|
71
|
+
+ "\n"
|
72
|
+
)
|
73
|
+
if lxml_error:
|
74
|
+
msg += lxml_error
|
75
|
+
if msg:
|
76
|
+
return msg.strip()
|
77
|
+
return "✅ Syntax valid"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from janito.i18n import tr
|
2
|
+
import re
|
3
|
+
|
4
|
+
|
5
|
+
def validate_js(file_path: str) -> str:
|
6
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
7
|
+
content = f.read()
|
8
|
+
errors = []
|
9
|
+
if content.count("{") != content.count("}"):
|
10
|
+
errors.append("Unmatched curly braces { }")
|
11
|
+
if content.count("(") != content.count(")"):
|
12
|
+
errors.append("Unmatched parentheses ( )")
|
13
|
+
if content.count("[") != content.count("]"):
|
14
|
+
errors.append("Unmatched brackets [ ]")
|
15
|
+
for quote in ["'", '"', "`"]:
|
16
|
+
unescaped = re.findall(rf"(?<!\\){quote}", content)
|
17
|
+
if len(unescaped) % 2 != 0:
|
18
|
+
errors.append(f"Unclosed string literal ({quote}) detected")
|
19
|
+
if content.count("/*") != content.count("*/"):
|
20
|
+
errors.append("Unclosed block comment (/* ... */)")
|
21
|
+
if errors:
|
22
|
+
msg = tr(
|
23
|
+
"⚠️ Warning: JavaScript syntax issues found:\n{errors}",
|
24
|
+
errors="\n".join(errors),
|
25
|
+
)
|
26
|
+
return msg
|
27
|
+
return "✅ Syntax valid"
|