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.
Files changed (142) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {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.tools.tools_utils import pluralize
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.tools.gitignore_utils import filter_ignored
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
- is_regex (bool): If True, treat pattern as regex. If False, treat as plain text. Defaults to False.
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 run(
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.report_warning(
55
- tr("⚠️ Warning: Empty search pattern provided. Operation skipped.")
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
- tr(
67
- "Invalid regex pattern: {error}. Falling back to no results.",
68
- error=e,
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
- output = []
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
- info_str = tr(
89
- "🔍 Searching for {search_type} '{pattern}' in '{disp_path}'",
90
- search_type=("text-regex" if use_regex else "text"),
91
- pattern=pattern,
92
- disp_path=display_path(search_path),
93
- )
94
- if max_depth > 0:
95
- info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
96
- self.report_info(info_str)
97
- dir_output = []
98
- dir_limit_reached = False
99
- if os.path.isfile(search_path):
100
- # Handle single file
101
- path = search_path
102
- if not is_binary_file(path):
103
- try:
104
- open_kwargs = {"mode": "r", "encoding": "utf-8"}
105
- if ignore_utf8_errors:
106
- open_kwargs["errors"] = "ignore"
107
- with open(path, **open_kwargs) as f:
108
- for lineno, line in enumerate(f, 1):
109
- if use_regex:
110
- if regex.search(line):
111
- dir_output.append(
112
- f"{path}:{lineno}: {line.strip()}"
113
- )
114
- else:
115
- if pattern in line:
116
- dir_output.append(
117
- f"{path}:{lineno}: {line.strip()}"
118
- )
119
- if (
120
- max_results > 0
121
- and (total_results + len(dir_output)) >= max_results
122
- ):
123
- dir_limit_reached = True
124
- break
125
- except Exception:
126
- pass
127
- output.extend(dir_output)
128
- total_results += len(dir_output)
129
- if dir_limit_reached:
130
- limit_reached = True
131
- break
132
- continue
133
- # Directory logic as before
134
- if max_depth == 1:
135
- walk_result = next(os.walk(search_path), None)
136
- if walk_result is None:
137
- walker = [(search_path, [], [])]
138
- else:
139
- _, dirs, files = walk_result
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
- walker = os.walk(search_path)
144
- stop_search = False
145
- for root, dirs, files in walker:
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
- rel_path = os.path.relpath(root, search_path)
149
- depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
150
- if max_depth == 1 and depth > 0:
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
- if max_depth > 0 and depth > max_depth:
153
- continue
154
- dirs, files = filter_ignored(root, dirs, files)
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
- " {count} {line_word} found{limit}",
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"
@@ -0,0 +1,6 @@
1
+ def validate_json(file_path: str) -> str:
2
+ import json
3
+
4
+ with open(file_path, "r", encoding="utf-8") as f:
5
+ json.load(f)
6
+ return "✅ Syntax valid"