janito 1.8.0__py3-none-any.whl → 1.9.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 (119) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config_defaults.py +23 -0
  3. janito/agent/config_utils.py +0 -9
  4. janito/agent/conversation.py +31 -9
  5. janito/agent/conversation_api.py +32 -2
  6. janito/agent/conversation_history.py +53 -0
  7. janito/agent/conversation_tool_calls.py +11 -8
  8. janito/agent/openai_client.py +11 -3
  9. janito/agent/openai_schema_generator.py +9 -6
  10. janito/agent/providers.py +77 -0
  11. janito/agent/rich_message_handler.py +1 -1
  12. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
  13. janito/agent/tool_executor.py +18 -10
  14. janito/agent/tool_use_tracker.py +16 -0
  15. janito/agent/tools/__init__.py +7 -9
  16. janito/agent/tools/create_directory.py +7 -6
  17. janito/agent/tools/create_file.py +29 -54
  18. janito/agent/tools/delete_text_in_file.py +97 -0
  19. janito/agent/tools/fetch_url.py +11 -3
  20. janito/agent/tools/find_files.py +37 -25
  21. janito/agent/tools/get_file_outline/__init__.py +1 -0
  22. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
  23. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  24. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
  25. janito/agent/tools/get_lines.py +15 -11
  26. janito/agent/tools/move_file.py +10 -11
  27. janito/agent/tools/remove_directory.py +2 -2
  28. janito/agent/tools/remove_file.py +11 -13
  29. janito/agent/tools/replace_file.py +62 -0
  30. janito/agent/tools/replace_text_in_file.py +3 -3
  31. janito/agent/tools/run_bash_command.py +3 -7
  32. janito/agent/tools/run_powershell_command.py +39 -28
  33. janito/agent/tools/run_python_command.py +3 -5
  34. janito/agent/tools/search_text.py +10 -14
  35. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  36. janito/agent/tools/validate_file_syntax/core.py +92 -0
  37. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  38. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  39. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  40. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  41. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  42. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  43. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  44. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  45. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  46. janito/agent/tools_utils/__init__.py +1 -0
  47. janito/agent/tools_utils/dir_walk_utils.py +23 -0
  48. janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
  49. janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
  50. janito/agent/tools_utils/utils.py +30 -0
  51. janito/cli/_livereload_log_utils.py +13 -0
  52. janito/cli/arg_parser.py +45 -3
  53. janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
  54. janito/cli/livereload_starter.py +60 -0
  55. janito/cli/main.py +110 -21
  56. janito/cli/one_shot.py +66 -0
  57. janito/cli/termweb_starter.py +2 -2
  58. janito/livereload/app.py +25 -0
  59. janito/rich_utils.py +0 -22
  60. janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
  61. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  62. janito/shell/commands/conversation_restart.py +72 -0
  63. janito/shell/commands/edit.py +21 -0
  64. janito/shell/commands/history_view.py +18 -0
  65. janito/shell/commands/livelogs.py +40 -0
  66. janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
  67. janito/shell/commands/session.py +32 -0
  68. janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
  69. janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
  70. janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
  71. janito/shell/commands/tools.py +23 -0
  72. janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
  73. janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
  74. janito/shell/commands.py +40 -0
  75. janito/shell/main.py +321 -0
  76. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  77. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  78. janito/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
  79. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
  80. janito/termweb/app.py +3 -3
  81. janito/termweb/static/editor.css +146 -0
  82. janito/termweb/static/editor.css.bak +27 -0
  83. janito/termweb/static/editor.html +15 -213
  84. janito/termweb/static/editor.html.bak +16 -215
  85. janito/termweb/static/editor.js +209 -0
  86. janito/termweb/static/editor.js.bak +227 -0
  87. janito/termweb/static/index.html +2 -3
  88. janito/termweb/static/index.html.bak +2 -3
  89. janito/termweb/static/termweb.css.bak +33 -84
  90. janito/termweb/static/termweb.js +15 -34
  91. janito/termweb/static/termweb.js.bak +18 -36
  92. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
  93. janito-1.9.0.dist-info/RECORD +151 -0
  94. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
  95. janito/agent/tools/dir_walk_utils.py +0 -16
  96. janito/agent/tools/memory.py +0 -48
  97. janito/agent/tools/outline_file/python_outline.py +0 -71
  98. janito/agent/tools/present_choices_test.py +0 -18
  99. janito/agent/tools/rich_live.py +0 -44
  100. janito/agent/tools/tools_utils.py +0 -56
  101. janito/agent/tools/utils.py +0 -33
  102. janito/agent/tools/validate_file_syntax.py +0 -163
  103. janito/cli_chat_shell/chat_loop.py +0 -163
  104. janito/cli_chat_shell/chat_state.py +0 -38
  105. janito/cli_chat_shell/commands/history_start.py +0 -37
  106. janito/cli_chat_shell/commands/session.py +0 -48
  107. janito-1.8.0.dist-info/RECORD +0 -127
  108. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  109. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  110. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  111. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  112. /janito/{cli/runner → shell}/__init__.py +0 -0
  113. /janito/{cli_chat_shell → shell}/commands/lang.py +0 -0
  114. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  115. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  116. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  117. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
  118. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
  119. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  import os
2
2
  import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
- from janito.agent.tools.utils import expand_path, display_path
4
+
5
+ # from janito.agent.tools_utils.expand_path import expand_path
6
+ from janito.agent.tools_utils.utils import display_path
5
7
  from janito.agent.tool_base import ToolBase
6
8
  from janito.i18n import tr
7
9
 
@@ -29,8 +31,8 @@ class MoveFileTool(ToolBase):
29
31
  ) -> str:
30
32
  original_src = src_path
31
33
  original_dest = dest_path
32
- src = expand_path(src_path)
33
- dest = expand_path(dest_path)
34
+ src = src_path # Using src_path as is
35
+ dest = dest_path # Using dest_path as is
34
36
  disp_src = display_path(original_src)
35
37
  disp_dest = display_path(original_dest)
36
38
  backup_path = None
@@ -82,19 +84,16 @@ class MoveFileTool(ToolBase):
82
84
  )
83
85
  return tr("❌ Error removing destination before move: {error}", error=e)
84
86
  try:
85
- shutil.move(src, dest)
86
- self.report_success(
87
+ self.report_info(
87
88
  tr(
88
- " Moved from '{disp_src}' to '{disp_dest}'",
89
+ "📝 Moving from '{disp_src}' to '{disp_dest}' ...",
89
90
  disp_src=disp_src,
90
91
  disp_dest=disp_dest,
91
92
  )
92
93
  )
93
- msg = tr(
94
- "✅ Successfully moved from '{disp_src}' to '{disp_dest}'.",
95
- disp_src=disp_src,
96
- disp_dest=disp_dest,
97
- )
94
+ shutil.move(src, dest)
95
+ self.report_success(tr("✅ Move complete."))
96
+ msg = tr("✅ Move complete.")
98
97
  if backup_path:
99
98
  msg += tr(
100
99
  " (backup at {backup_disp})",
@@ -1,6 +1,6 @@
1
1
  from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize, display_path
3
+ from janito.agent.tools_utils.utils import pluralize, display_path
4
4
  from janito.i18n import tr
5
5
  import shutil
6
6
  import os
@@ -24,7 +24,7 @@ class RemoveDirectoryTool(ToolBase):
24
24
  def run(self, file_path: str, recursive: bool = False) -> str:
25
25
  disp_path = display_path(file_path)
26
26
  self.report_info(
27
- tr("🗃️ Removing directory: {disp_path} ...", disp_path=disp_path)
27
+ tr("🗃️ Removing directory '{disp_path}' ...", disp_path=disp_path)
28
28
  )
29
29
  backup_zip = None
30
30
  try:
@@ -1,7 +1,9 @@
1
1
  import os
2
2
  import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
- from janito.agent.tools.utils import expand_path, display_path
4
+
5
+ # from janito.agent.tools_utils.expand_path import expand_path
6
+ from janito.agent.tools_utils.utils import display_path
5
7
  from janito.agent.tool_base import ToolBase
6
8
  from janito.i18n import tr
7
9
 
@@ -22,27 +24,23 @@ class RemoveFileTool(ToolBase):
22
24
 
23
25
  def run(self, file_path: str, backup: bool = False) -> str:
24
26
  original_path = file_path
25
- path = expand_path(file_path)
27
+ path = file_path # Using file_path as is
26
28
  disp_path = display_path(original_path)
27
29
  backup_path = None
30
+ # Report initial info about what is going to be removed
31
+ self.report_info(tr("🗑️ Removing file '{disp_path}' ...", disp_path=disp_path))
28
32
  if not os.path.exists(path):
29
- self.report_error(
30
- tr("❌ File '{disp_path}' does not exist.", disp_path=disp_path)
31
- )
32
- return tr("❌ File '{disp_path}' does not exist.", disp_path=disp_path)
33
+ self.report_error(tr("❌ File does not exist."))
34
+ return tr("❌ File does not exist.")
33
35
  if not os.path.isfile(path):
34
- self.report_error(
35
- tr("❌ Path '{disp_path}' is not a file.", disp_path=disp_path)
36
- )
37
- return tr("❌ Path '{disp_path}' is not a file.", disp_path=disp_path)
36
+ self.report_error(tr("❌ Path is not a file."))
37
+ return tr("❌ Path is not a file.")
38
38
  try:
39
39
  if backup:
40
40
  backup_path = path + ".bak"
41
41
  shutil.copy2(path, backup_path)
42
42
  os.remove(path)
43
- self.report_success(
44
- tr("✅ File removed: '{disp_path}'", disp_path=disp_path)
45
- )
43
+ self.report_success(tr("✅ File removed"))
46
44
  msg = tr(
47
45
  "✅ Successfully removed the file at '{disp_path}'.",
48
46
  disp_path=disp_path,
@@ -0,0 +1,62 @@
1
+ import os
2
+ import shutil
3
+ from janito.agent.tool_registry import register_tool
4
+ from janito.agent.tools_utils.utils import display_path
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.i18n import tr
7
+
8
+ from janito.agent.tools.validate_file_syntax.core import validate_file_syntax
9
+
10
+
11
+ @register_tool(name="replace_file")
12
+ class ReplaceFileTool(ToolBase):
13
+ """
14
+ Replace the entire content of an existing file. Fails if the file does not exist.
15
+ Args:
16
+ file_path (str): Path to the file to replace content.
17
+ content (str): The full new content to write to the file. You must provide the complete content as it will fully overwrite the existing file—do not use placeholders for original content.
18
+ Returns:
19
+ str: Status message indicating the result. Example:
20
+ - "✅ Successfully replaced the file at ..."
21
+ """
22
+
23
+ def run(self, file_path: str, content: str) -> str:
24
+ from janito.agent.tool_use_tracker import ToolUseTracker
25
+
26
+ expanded_file_path = file_path # Using file_path as is
27
+ disp_path = display_path(expanded_file_path)
28
+ file_path = expanded_file_path
29
+ if not os.path.exists(file_path):
30
+ return tr(
31
+ "❗ Cannot replace: file does not exist at '{disp_path}'.",
32
+ disp_path=disp_path,
33
+ )
34
+ # Check previous operation
35
+ tracker = ToolUseTracker()
36
+ if not tracker.last_operation_is_full_read_or_replace(file_path):
37
+ self.report_warning(tr("ℹ️ Missing full view."))
38
+ try:
39
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
40
+ current_content = f.read()
41
+ except Exception as e:
42
+ current_content = f"[Error reading file: {e}]"
43
+ return (
44
+ "⚠️ [missing full view] You must fully view or replace the file before updating.\n"
45
+ f"--- Current content of {disp_path} ---\n"
46
+ f"{current_content}"
47
+ )
48
+ self.report_info(tr("📝 Replacing file '{disp_path}' ...", disp_path=disp_path))
49
+ backup_path = file_path + ".bak"
50
+ shutil.copy2(file_path, backup_path)
51
+ with open(file_path, "w", encoding="utf-8", errors="replace") as f:
52
+ f.write(content)
53
+ new_lines = content.count("\n") + 1 if content else 0
54
+ self.report_success(tr("✅ {new_lines} lines", new_lines=new_lines))
55
+ msg = tr(
56
+ "✅ Replaced file ({new_lines} lines, backup at {backup_path}).",
57
+ new_lines=new_lines,
58
+ backup_path=backup_path,
59
+ )
60
+ # Perform syntax validation and append result
61
+ validation_result = validate_file_syntax(file_path)
62
+ return msg + f"\n{validation_result}"
@@ -29,7 +29,7 @@ class ReplaceTextInFileTool(ToolBase):
29
29
  replace_all: bool = False,
30
30
  backup: bool = False,
31
31
  ) -> str:
32
- from janito.agent.tools.tools_utils import display_path
32
+ from janito.agent.tools_utils.utils import display_path
33
33
 
34
34
  disp_path = display_path(file_path)
35
35
  action = "(all)" if replace_all else "(unique)"
@@ -95,7 +95,7 @@ class ReplaceTextInFileTool(ToolBase):
95
95
  else:
96
96
  occurrences = content.count(search_text)
97
97
  if occurrences > 1:
98
- self.report_warning(tr("⚠️ Search text is not unique."))
98
+ self.report_warning(tr(" ℹ️ No changes made. [not unique]"))
99
99
  warning_detail = tr(
100
100
  "The search text is not unique. Expand your search context with surrounding lines to ensure uniqueness."
101
101
  )
@@ -120,7 +120,7 @@ class ReplaceTextInFileTool(ToolBase):
120
120
  if replaced_count == 0:
121
121
  warning = tr(" [Warning: Search text not found in file]")
122
122
  if not file_changed:
123
- self.report_warning(tr(" ℹ️ No changes made."))
123
+ self.report_warning(tr(" ℹ️ No changes made. [not found]"))
124
124
  concise_warning = tr(
125
125
  "The search text was not found. Expand your search context with surrounding lines if needed."
126
126
  )
@@ -29,15 +29,11 @@ class RunBashCommandTool(ToolBase):
29
29
  interactive: bool = False,
30
30
  ) -> str:
31
31
  if not command.strip():
32
- self.report_warning(
33
- tr("⚠️ Warning: Empty command provided. Operation skipped.")
34
- )
32
+ self.report_warning(tr("ℹ️ Empty command provided."))
35
33
  return tr("Warning: Empty command provided. Operation skipped.")
36
- self.report_info(
37
- tr("🖥️ Running bash command: {command} ...\n", command=command)
38
- )
34
+ self.report_info(tr("🖥️ Running bash command: {command} ...\n", command=command))
39
35
  if interactive:
40
- self.report_info(
36
+ self.report_warning(
41
37
  tr(
42
38
  "⚠️ Warning: This command might be interactive, require user input, and might hang."
43
39
  )
@@ -3,6 +3,7 @@ from janito.agent.tool_registry import register_tool
3
3
  from janito.i18n import tr
4
4
  import subprocess
5
5
  import tempfile
6
+ import threading
6
7
 
7
8
 
8
9
  @register_tool(name="run_powershell_command")
@@ -30,17 +31,15 @@ class RunPowerShellCommandTool(ToolBase):
30
31
  interactive: bool = False,
31
32
  ) -> str:
32
33
  if not command.strip():
33
- self.report_warning(
34
- tr("⚠️ Warning: Empty command provided. Operation skipped.")
35
- )
34
+ self.report_warning(tr("ℹ️ Empty command provided."))
36
35
  return tr("Warning: Empty command provided. Operation skipped.")
37
36
  encoding_prefix = "$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
38
37
  command_with_encoding = encoding_prefix + command
39
38
  self.report_info(
40
- tr("🖥️ Running PowerShell command: {command} ...\n", command=command)
39
+ tr("🖥️ Running PowerShell command: {command} ...\n", command=command)
41
40
  )
42
41
  if interactive:
43
- self.report_info(
42
+ self.report_warning(
44
43
  tr(
45
44
  "⚠️ Warning: This command might be interactive, require user input, and might hang."
46
45
  )
@@ -53,7 +52,7 @@ class RunPowerShellCommandTool(ToolBase):
53
52
  )
54
53
  )
55
54
  if not confirmed:
56
- self.report_warning(tr("Execution cancelled by user."))
55
+ self.report_warning(tr("⚠️ Execution cancelled by user."))
57
56
  return tr("❌ Command execution cancelled by user.")
58
57
  from janito.agent.platform_discovery import is_windows
59
58
 
@@ -82,10 +81,38 @@ class RunPowerShellCommandTool(ToolBase):
82
81
  "-Command",
83
82
  command_with_encoding,
84
83
  ],
85
- stdout=stdout_file,
86
- stderr=stderr_file,
84
+ stdout=subprocess.PIPE,
85
+ stderr=subprocess.PIPE,
87
86
  text=True,
87
+ bufsize=1,
88
+ universal_newlines=True,
89
+ encoding="utf-8",
88
90
  )
91
+
92
+ stdout_lines = 0
93
+ stderr_lines = 0
94
+
95
+ def stream_output(stream, file_obj, report_func, count_func):
96
+ nonlocal stdout_lines, stderr_lines
97
+ for line in stream:
98
+ file_obj.write(line)
99
+ file_obj.flush()
100
+ report_func(line)
101
+ if count_func == "stdout":
102
+ stdout_lines += 1
103
+ else:
104
+ stderr_lines += 1
105
+
106
+ stdout_thread = threading.Thread(
107
+ target=stream_output,
108
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
109
+ )
110
+ stderr_thread = threading.Thread(
111
+ target=stream_output,
112
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
113
+ )
114
+ stdout_thread.start()
115
+ stderr_thread.start()
89
116
  try:
90
117
  return_code = process.wait(timeout=timeout)
91
118
  except subprocess.TimeoutExpired:
@@ -96,28 +123,11 @@ class RunPowerShellCommandTool(ToolBase):
96
123
  return tr(
97
124
  "Command timed out after {timeout} seconds.", timeout=timeout
98
125
  )
126
+ stdout_thread.join()
127
+ stderr_thread.join()
99
128
  stdout_file.flush()
100
129
  stderr_file.flush()
101
- with open(
102
- stdout_file.name, "r", encoding="utf-8", errors="replace"
103
- ) as out_f:
104
- out_f.seek(0)
105
- for line in out_f:
106
- self.report_stdout(line)
107
- with open(
108
- stderr_file.name, "r", encoding="utf-8", errors="replace"
109
- ) as err_f:
110
- err_f.seek(0)
111
- for line in err_f:
112
- self.report_stderr(line)
113
- with open(
114
- stdout_file.name, "r", encoding="utf-8", errors="replace"
115
- ) as out_f:
116
- stdout_lines = sum(1 for _ in out_f)
117
- with open(
118
- stderr_file.name, "r", encoding="utf-8", errors="replace"
119
- ) as err_f:
120
- stderr_lines = sum(1 for _ in err_f)
130
+
121
131
  self.report_success(
122
132
  tr(" ✅ return code {return_code}", return_code=return_code)
123
133
  )
@@ -126,6 +136,7 @@ class RunPowerShellCommandTool(ToolBase):
126
136
  warning_msg = tr(
127
137
  "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
128
138
  )
139
+ # Read back the content for summary if not too large
129
140
  with open(
130
141
  stdout_file.name, "r", encoding="utf-8", errors="replace"
131
142
  ) as out_f:
@@ -28,13 +28,11 @@ class RunPythonCommandTool(ToolBase):
28
28
  interactive: bool = False,
29
29
  ) -> str:
30
30
  if not code.strip():
31
- self.report_warning(
32
- tr("⚠️ Warning: Empty code provided. Operation skipped.")
33
- )
31
+ self.report_warning(tr("ℹ️ Empty code provided."))
34
32
  return tr("Warning: Empty code provided. Operation skipped.")
35
33
  self.report_info(tr("🐍 Running Python code: ...\n{code}\n", code=code))
36
34
  if interactive:
37
- self.report_info(
35
+ self.report_warning(
38
36
  tr(
39
37
  "⚠️ Warning: This code might be interactive, require user input, and might hang."
40
38
  )
@@ -45,7 +43,7 @@ class RunPythonCommandTool(ToolBase):
45
43
  tr("Do you want to execute this Python code?")
46
44
  )
47
45
  if not confirmed:
48
- self.report_warning(tr("Execution cancelled by user."))
46
+ self.report_warning(tr("⚠️ Execution cancelled by user."))
49
47
  return tr("Execution cancelled by user.")
50
48
  try:
51
49
  with (
@@ -1,10 +1,10 @@
1
1
  from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize
3
+ from janito.agent.tools_utils.utils import pluralize
4
4
  from janito.i18n import tr
5
5
  import os
6
6
  import re
7
- from janito.agent.tools.gitignore_utils import filter_ignored
7
+ from janito.agent.tools_utils.gitignore_utils import filter_ignored
8
8
 
9
9
 
10
10
  def is_binary_file(path, blocksize=1024):
@@ -31,7 +31,8 @@ class SearchTextTool(ToolBase):
31
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.
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]').
35
36
  is_regex (bool): If True, treat pattern as regex. If False, treat as plain text. Defaults to False.
36
37
  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
38
  max_results (int): Maximum number of results to return. 0 means no limit (default).
@@ -51,10 +52,10 @@ class SearchTextTool(ToolBase):
51
52
  ignore_utf8_errors: bool = True,
52
53
  ) -> str:
53
54
  if not pattern:
54
- self.report_warning(
55
- tr("⚠️ Warning: Empty search pattern provided. Operation skipped.")
55
+ self.report_error(
56
+ tr("Error: Empty search pattern provided. Operation aborted.")
56
57
  )
57
- return tr("Warning: Empty search pattern provided. Operation skipped.")
58
+ return tr("Error: Empty search pattern provided. Operation aborted.")
58
59
  regex = None
59
60
  use_regex = False
60
61
  if is_regex:
@@ -62,12 +63,7 @@ class SearchTextTool(ToolBase):
62
63
  regex = re.compile(pattern)
63
64
  use_regex = True
64
65
  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
- )
66
+ self.report_warning(tr("⚠️ Invalid regex pattern."))
71
67
  return tr(
72
68
  "Warning: Invalid regex pattern: {error}. No results.", error=e
73
69
  )
@@ -83,7 +79,7 @@ class SearchTextTool(ToolBase):
83
79
  total_results = 0
84
80
  paths_list = paths.split()
85
81
  for search_path in paths_list:
86
- from janito.agent.tools.tools_utils import display_path
82
+ from janito.agent.tools_utils.utils import display_path
87
83
 
88
84
  info_str = tr(
89
85
  "🔍 Searching for {search_type} '{pattern}' in '{disp_path}'",
@@ -199,7 +195,7 @@ class SearchTextTool(ToolBase):
199
195
  result += tr("\n[Note: max_results limit reached, output truncated.]")
200
196
  self.report_success(
201
197
  tr(
202
- " ✅ {count} {line_word} found{limit}",
198
+ " ✅ {count} {line_word}{limit}",
203
199
  count=len(output),
204
200
  line_word=pluralize("line", len(output)),
205
201
  limit=(" (limit reached)" if limit_reached else ""),
@@ -0,0 +1 @@
1
+ # Validation syntax package
@@ -0,0 +1,92 @@
1
+ import os
2
+ from janito.i18n import tr
3
+ from janito.agent.tool_base import ToolBase
4
+ from janito.agent.tool_registry import register_tool
5
+ from janito.agent.tools_utils.utils import display_path
6
+
7
+ from .python_validator import validate_python
8
+ from .json_validator import validate_json
9
+ from .yaml_validator import validate_yaml
10
+ from .ps1_validator import validate_ps1
11
+ from .xml_validator import validate_xml
12
+ from .html_validator import validate_html
13
+ from .markdown_validator import validate_markdown
14
+ from .js_validator import validate_js
15
+ from .css_validator import validate_css
16
+
17
+
18
+ def validate_file_syntax(
19
+ file_path: str, report_info=None, report_warning=None, report_success=None
20
+ ) -> str:
21
+ ext = os.path.splitext(file_path)[1].lower()
22
+ try:
23
+ if ext in [".py", ".pyw"]:
24
+ return validate_python(file_path)
25
+ elif ext == ".json":
26
+ return validate_json(file_path)
27
+ elif ext in [".yml", ".yaml"]:
28
+ return validate_yaml(file_path)
29
+ elif ext == ".ps1":
30
+ return validate_ps1(file_path)
31
+ elif ext == ".xml":
32
+ return validate_xml(file_path)
33
+ elif ext in (".html", ".htm"):
34
+ return validate_html(file_path)
35
+ elif ext == ".md":
36
+ return validate_markdown(file_path)
37
+ elif ext == ".js":
38
+ return validate_js(file_path)
39
+ elif ext == ".css":
40
+ return validate_css(file_path)
41
+ else:
42
+ msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
43
+ if report_warning:
44
+ report_warning(msg)
45
+ return msg
46
+ except Exception as e:
47
+ msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
48
+ if report_warning:
49
+ report_warning(msg)
50
+ return msg
51
+
52
+
53
+ @register_tool(name="validate_file_syntax")
54
+ class ValidateFileSyntaxTool(ToolBase):
55
+ """
56
+ Validate a file for syntax issues.
57
+
58
+ Supported types:
59
+ - Python (.py, .pyw)
60
+ - JSON (.json)
61
+ - YAML (.yml, .yaml)
62
+ - PowerShell (.ps1)
63
+ - XML (.xml)
64
+ - HTML (.html, .htm) [lxml]
65
+ - Markdown (.md)
66
+ - JavaScript (.js)
67
+
68
+ Args:
69
+ file_path (str): Path to the file to validate.
70
+ Returns:
71
+ str: Validation status message. Example:
72
+ - "✅ Syntax OK"
73
+ - "⚠️ Warning: Syntax error: <error message>"
74
+ - "⚠️ Warning: Unsupported file extension: <ext>"
75
+ """
76
+
77
+ def run(self, file_path: str) -> str:
78
+ disp_path = display_path(file_path)
79
+ self.report_info(
80
+ tr("🔎 Validating syntax for file '{disp_path}' ...", disp_path=disp_path)
81
+ )
82
+ result = validate_file_syntax(
83
+ file_path,
84
+ report_info=self.report_info,
85
+ report_warning=self.report_warning,
86
+ report_success=self.report_success,
87
+ )
88
+ if result.startswith("✅"):
89
+ self.report_success(result)
90
+ elif result.startswith("⚠️"):
91
+ self.report_warning(tr("⚠️ ") + result.lstrip("⚠️ "))
92
+ 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"