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.
- janito/__init__.py +1 -1
- janito/agent/config_defaults.py +23 -0
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +31 -9
- janito/agent/conversation_api.py +32 -2
- janito/agent/conversation_history.py +53 -0
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/openai_client.py +11 -3
- janito/agent/openai_schema_generator.py +9 -6
- janito/agent/providers.py +77 -0
- janito/agent/rich_message_handler.py +1 -1
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
- janito/agent/tool_executor.py +18 -10
- janito/agent/tool_use_tracker.py +16 -0
- janito/agent/tools/__init__.py +7 -9
- janito/agent/tools/create_directory.py +7 -6
- janito/agent/tools/create_file.py +29 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +11 -3
- janito/agent/tools/find_files.py +37 -25
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
- janito/agent/tools/get_lines.py +15 -11
- janito/agent/tools/move_file.py +10 -11
- janito/agent/tools/remove_directory.py +2 -2
- janito/agent/tools/remove_file.py +11 -13
- janito/agent/tools/replace_file.py +62 -0
- janito/agent/tools/replace_text_in_file.py +3 -3
- janito/agent/tools/run_bash_command.py +3 -7
- janito/agent/tools/run_powershell_command.py +39 -28
- janito/agent/tools/run_python_command.py +3 -5
- janito/agent/tools/search_text.py +10 -14
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +92 -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/dir_walk_utils.py +23 -0
- janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
- janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/arg_parser.py +45 -3
- janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +110 -21
- janito/cli/one_shot.py +66 -0
- janito/cli/termweb_starter.py +2 -2
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +0 -22
- janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +72 -0
- janito/shell/commands/edit.py +21 -0
- janito/shell/commands/history_view.py +18 -0
- janito/shell/commands/livelogs.py +40 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
- janito/shell/commands/session.py +32 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
- janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
- janito/shell/commands/tools.py +23 -0
- janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
- janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
- janito/shell/commands.py +40 -0
- janito/shell/main.py +321 -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/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +146 -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-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
- janito-1.9.0.dist-info/RECORD +151 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/memory.py +0 -48
- 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/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- 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-1.8.0.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}/commands/lang.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.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/top_level.txt +0 -0
janito/agent/tools/move_file.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
import os
|
2
2
|
import shutil
|
3
3
|
from janito.agent.tool_registry import register_tool
|
4
|
-
|
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 =
|
33
|
-
dest =
|
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
|
-
|
86
|
-
self.report_success(
|
87
|
+
self.report_info(
|
87
88
|
tr(
|
88
|
-
"
|
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
|
-
|
94
|
-
|
95
|
-
|
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.
|
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("🗃️
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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.
|
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("
|
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.
|
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("🖥️
|
39
|
+
tr("🖥️ Running PowerShell command: {command} ...\n", command=command)
|
41
40
|
)
|
42
41
|
if interactive:
|
43
|
-
self.
|
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=
|
86
|
-
stderr=
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
55
|
-
tr("
|
55
|
+
self.report_error(
|
56
|
+
tr("Error: Empty search pattern provided. Operation aborted.")
|
56
57
|
)
|
57
|
-
return tr("
|
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.
|
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}
|
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"
|