janito 1.8.1__py3-none-any.whl → 1.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- janito/__init__.py +1 -1
- janito/agent/api_exceptions.py +4 -0
- janito/agent/config.py +1 -1
- janito/agent/config_defaults.py +2 -3
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +177 -114
- janito/agent/conversation_api.py +179 -159
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/llm_conversation_history.py +70 -0
- janito/agent/openai_client.py +44 -21
- janito/agent/openai_schema_generator.py +164 -128
- janito/agent/platform_discovery.py +134 -77
- janito/agent/profile_manager.py +5 -5
- janito/agent/rich_message_handler.py +80 -31
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
- janito/agent/test_openai_schema_generator.py +93 -0
- janito/agent/tool_base.py +7 -2
- janito/agent/tool_executor.py +63 -50
- janito/agent/tool_registry.py +5 -2
- janito/agent/tool_use_tracker.py +42 -5
- janito/agent/tools/__init__.py +13 -12
- janito/agent/tools/create_directory.py +9 -6
- janito/agent/tools/create_file.py +35 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +50 -5
- janito/agent/tools/find_files.py +40 -26
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
- janito/agent/tools/get_lines.py +21 -12
- janito/agent/tools/move_file.py +13 -12
- janito/agent/tools/present_choices.py +3 -1
- janito/agent/tools/python_command_runner.py +150 -0
- janito/agent/tools/python_file_runner.py +148 -0
- janito/agent/tools/python_stdin_runner.py +154 -0
- janito/agent/tools/remove_directory.py +4 -2
- janito/agent/tools/remove_file.py +15 -13
- janito/agent/tools/replace_file.py +72 -0
- janito/agent/tools/replace_text_in_file.py +7 -5
- janito/agent/tools/run_bash_command.py +29 -72
- janito/agent/tools/run_powershell_command.py +142 -102
- janito/agent/tools/search_text.py +177 -131
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +94 -0
- janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
- janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
- janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
- janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
- janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
- janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
- janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
- janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
- janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
- janito/agent/tools_utils/__init__.py +1 -0
- janito/agent/tools_utils/action_type.py +7 -0
- janito/agent/tools_utils/dir_walk_utils.py +24 -0
- janito/agent/tools_utils/formatting.py +49 -0
- janito/agent/tools_utils/gitignore_utils.py +69 -0
- janito/agent/tools_utils/test_gitignore_utils.py +46 -0
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/_print_config.py +63 -61
- janito/cli/arg_parser.py +57 -14
- janito/cli/cli_main.py +270 -0
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +166 -99
- janito/cli/one_shot.py +80 -0
- janito/cli/termweb_starter.py +2 -2
- janito/i18n/__init__.py +1 -1
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +41 -25
- janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +74 -0
- janito/shell/commands/edit.py +24 -0
- janito/shell/commands/history_view.py +18 -0
- janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
- janito/shell/commands/livelogs.py +42 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
- janito/shell/commands/session.py +35 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
- janito/shell/commands/tools.py +26 -0
- janito/shell/commands/track.py +36 -0
- janito/shell/commands/utility.py +28 -0
- janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
- janito/shell/commands.py +40 -0
- janito/shell/input_history.py +62 -0
- janito/shell/main.py +257 -0
- janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
- janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
- janito/shell/session/manager.py +101 -0
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +142 -0
- janito/termweb/static/editor.css.bak +27 -0
- janito/termweb/static/editor.html +15 -213
- janito/termweb/static/editor.html.bak +16 -215
- janito/termweb/static/editor.js +209 -0
- janito/termweb/static/editor.js.bak +227 -0
- janito/termweb/static/index.html +2 -3
- janito/termweb/static/index.html.bak +2 -3
- janito/termweb/static/termweb.css.bak +33 -84
- janito/termweb/static/termweb.js +15 -34
- janito/termweb/static/termweb.js.bak +18 -36
- janito/tests/test_rich_utils.py +44 -0
- janito/web/app.py +0 -75
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
- janito-1.10.0.dist-info/RECORD +158 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/gitignore_utils.py +0 -46
- janito/agent/tools/memory.py +0 -48
- janito/agent/tools/outline_file/formatting.py +0 -20
- janito/agent/tools/outline_file/python_outline.py +0 -71
- janito/agent/tools/present_choices_test.py +0 -18
- janito/agent/tools/rich_live.py +0 -44
- janito/agent/tools/run_python_command.py +0 -163
- janito/agent/tools/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- janito/cli/runner/cli_main.py +0 -180
- janito/cli_chat_shell/chat_loop.py +0 -163
- janito/cli_chat_shell/chat_state.py +0 -38
- janito/cli_chat_shell/commands/history_start.py +0 -37
- janito/cli_chat_shell/commands/session.py +0 -48
- janito/cli_chat_shell/commands/sum.py +0 -49
- janito/cli_chat_shell/commands/utility.py +0 -32
- janito/cli_chat_shell/session_manager.py +0 -72
- janito-1.8.1.dist-info/RECORD +0 -127
- /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
- /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
- /janito/cli/{runner/config.py → config_runner.py} +0 -0
- /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
- /janito/{cli/runner → shell}/__init__.py +0 -0
- /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
- /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
- /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,11 @@
|
|
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
|
8
|
+
from janito.agent.tools_utils.action_type import ActionType
|
6
9
|
from janito.i18n import tr
|
7
10
|
|
8
11
|
|
@@ -22,27 +25,26 @@ class RemoveFileTool(ToolBase):
|
|
22
25
|
|
23
26
|
def run(self, file_path: str, backup: bool = False) -> str:
|
24
27
|
original_path = file_path
|
25
|
-
path =
|
28
|
+
path = file_path # Using file_path as is
|
26
29
|
disp_path = display_path(original_path)
|
27
30
|
backup_path = None
|
31
|
+
# Report initial info about what is going to be removed
|
32
|
+
self.report_info(
|
33
|
+
ActionType.WRITE,
|
34
|
+
tr("🗑️ Removing file '{disp_path}' ...", disp_path=disp_path),
|
35
|
+
)
|
28
36
|
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)
|
37
|
+
self.report_error(tr("❌ File does not exist."))
|
38
|
+
return tr("❌ File does not exist.")
|
33
39
|
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)
|
40
|
+
self.report_error(tr("❌ Path is not a file."))
|
41
|
+
return tr("❌ Path is not a file.")
|
38
42
|
try:
|
39
43
|
if backup:
|
40
44
|
backup_path = path + ".bak"
|
41
45
|
shutil.copy2(path, backup_path)
|
42
46
|
os.remove(path)
|
43
|
-
self.report_success(
|
44
|
-
tr("✅ File removed: '{disp_path}'", disp_path=disp_path)
|
45
|
-
)
|
47
|
+
self.report_success(tr("✅ File removed"))
|
46
48
|
msg = tr(
|
47
49
|
"✅ Successfully removed the file at '{disp_path}'.",
|
48
50
|
disp_path=disp_path,
|
@@ -0,0 +1,72 @@
|
|
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.agent.tools_utils.action_type import ActionType
|
7
|
+
from janito.i18n import tr
|
8
|
+
|
9
|
+
from janito.agent.tools.validate_file_syntax.core import validate_file_syntax
|
10
|
+
|
11
|
+
|
12
|
+
@register_tool(name="replace_file")
|
13
|
+
class ReplaceFileTool(ToolBase):
|
14
|
+
"""
|
15
|
+
Replace the entire content of an existing file. Fails if the file does not exist.
|
16
|
+
Args:
|
17
|
+
file_path (str): Path to the file to replace content.
|
18
|
+
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.
|
19
|
+
Returns:
|
20
|
+
str: Status message indicating the result. Example:
|
21
|
+
- "✅ Successfully replaced the file at ..."
|
22
|
+
|
23
|
+
Note: Syntax validation is automatically performed after this operation.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def run(self, file_path: str, content: str) -> str:
|
27
|
+
from janito.agent.tool_use_tracker import ToolUseTracker
|
28
|
+
|
29
|
+
expanded_file_path = file_path # Using file_path as is
|
30
|
+
disp_path = display_path(expanded_file_path)
|
31
|
+
file_path = expanded_file_path
|
32
|
+
if not os.path.exists(file_path):
|
33
|
+
return tr(
|
34
|
+
"❗ Cannot replace: file does not exist at '{disp_path}'.",
|
35
|
+
disp_path=disp_path,
|
36
|
+
)
|
37
|
+
# Check previous operation
|
38
|
+
tracker = ToolUseTracker()
|
39
|
+
if not tracker.last_operation_is_full_read_or_replace(file_path):
|
40
|
+
self.report_info(
|
41
|
+
ActionType.WRITE,
|
42
|
+
tr("📝 Replacing file '{disp_path}' ...", disp_path=disp_path),
|
43
|
+
)
|
44
|
+
self.report_warning(tr("ℹ️ Missing full view."))
|
45
|
+
try:
|
46
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
47
|
+
current_content = f.read()
|
48
|
+
except Exception as e:
|
49
|
+
current_content = f"[Error reading file: {e}]"
|
50
|
+
return (
|
51
|
+
"⚠️ [missing full view] Update was NOT performed. The full content of the file is included below for your review. Repeat the operation if you wish to proceed.\n"
|
52
|
+
f"--- Current content of {disp_path} ---\n"
|
53
|
+
f"{current_content}"
|
54
|
+
)
|
55
|
+
self.report_info(
|
56
|
+
ActionType.WRITE,
|
57
|
+
tr("📝 Replacing file '{disp_path}' ...", disp_path=disp_path),
|
58
|
+
)
|
59
|
+
backup_path = file_path + ".bak"
|
60
|
+
shutil.copy2(file_path, backup_path)
|
61
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
62
|
+
f.write(content)
|
63
|
+
new_lines = content.count("\n") + 1 if content else 0
|
64
|
+
self.report_success(tr("✅ {new_lines} lines", new_lines=new_lines))
|
65
|
+
msg = tr(
|
66
|
+
"✅ Replaced file ({new_lines} lines, backup at {backup_path}).",
|
67
|
+
new_lines=new_lines,
|
68
|
+
backup_path=backup_path,
|
69
|
+
)
|
70
|
+
# Perform syntax validation and append result
|
71
|
+
validation_result = validate_file_syntax(file_path)
|
72
|
+
return msg + f"\n{validation_result}"
|
@@ -1,4 +1,5 @@
|
|
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
4
|
from janito.i18n import tr
|
4
5
|
|
@@ -29,10 +30,10 @@ class ReplaceTextInFileTool(ToolBase):
|
|
29
30
|
replace_all: bool = False,
|
30
31
|
backup: bool = False,
|
31
32
|
) -> str:
|
32
|
-
from janito.agent.
|
33
|
+
from janito.agent.tools_utils.utils import display_path
|
33
34
|
|
34
35
|
disp_path = display_path(file_path)
|
35
|
-
action = "(all)" if replace_all else "
|
36
|
+
action = "(all)" if replace_all else ""
|
36
37
|
search_lines = len(search_text.splitlines())
|
37
38
|
replace_lines = len(replacement_text.splitlines())
|
38
39
|
if replace_lines == 0:
|
@@ -67,7 +68,8 @@ class ReplaceTextInFileTool(ToolBase):
|
|
67
68
|
action=action,
|
68
69
|
)
|
69
70
|
self.report_info(
|
70
|
-
|
71
|
+
ActionType.WRITE,
|
72
|
+
info_msg + (" ..." if not info_msg.rstrip().endswith("...") else ""),
|
71
73
|
)
|
72
74
|
try:
|
73
75
|
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
@@ -95,7 +97,7 @@ class ReplaceTextInFileTool(ToolBase):
|
|
95
97
|
else:
|
96
98
|
occurrences = content.count(search_text)
|
97
99
|
if occurrences > 1:
|
98
|
-
self.report_warning(tr("
|
100
|
+
self.report_warning(tr(" ℹ️ No changes made. [not unique]"))
|
99
101
|
warning_detail = tr(
|
100
102
|
"The search text is not unique. Expand your search context with surrounding lines to ensure uniqueness."
|
101
103
|
)
|
@@ -120,7 +122,7 @@ class ReplaceTextInFileTool(ToolBase):
|
|
120
122
|
if replaced_count == 0:
|
121
123
|
warning = tr(" [Warning: Search text not found in file]")
|
122
124
|
if not file_changed:
|
123
|
-
self.report_warning(tr(" ℹ️ No changes made."))
|
125
|
+
self.report_warning(tr(" ℹ️ No changes made. [not found]"))
|
124
126
|
concise_warning = tr(
|
125
127
|
"The search text was not found. Expand your search context with surrounding lines if needed."
|
126
128
|
)
|
@@ -1,4 +1,5 @@
|
|
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
4
|
from janito.i18n import tr
|
4
5
|
import subprocess
|
@@ -16,7 +17,7 @@ class RunBashCommandTool(ToolBase):
|
|
16
17
|
command (str): The bash command to execute.
|
17
18
|
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
18
19
|
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
19
|
-
|
20
|
+
requires_user_input (bool, optional): If True, warns that the command may require user input and might hang. Defaults to False. Non-interactive commands are preferred for automation and reliability.
|
20
21
|
Returns:
|
21
22
|
str: File paths and line counts for stdout and stderr.
|
22
23
|
"""
|
@@ -26,20 +27,19 @@ class RunBashCommandTool(ToolBase):
|
|
26
27
|
command: str,
|
27
28
|
timeout: int = 60,
|
28
29
|
require_confirmation: bool = False,
|
29
|
-
|
30
|
+
requires_user_input: bool = False,
|
30
31
|
) -> str:
|
31
32
|
if not command.strip():
|
32
|
-
self.report_warning(
|
33
|
-
tr("⚠️ Warning: Empty command provided. Operation skipped.")
|
34
|
-
)
|
33
|
+
self.report_warning(tr("\u2139\ufe0f Empty command provided."))
|
35
34
|
return tr("Warning: Empty command provided. Operation skipped.")
|
36
35
|
self.report_info(
|
37
|
-
|
36
|
+
ActionType.EXECUTE,
|
37
|
+
tr("🖥️ Running bash command: {command} ...\n", command=command),
|
38
38
|
)
|
39
|
-
if
|
40
|
-
self.
|
39
|
+
if requires_user_input:
|
40
|
+
self.report_warning(
|
41
41
|
tr(
|
42
|
-
"
|
42
|
+
"\u26a0\ufe0f Warning: This command might be interactive, require user input, and might hang."
|
43
43
|
)
|
44
44
|
)
|
45
45
|
sys.stdout.flush()
|
@@ -65,88 +65,45 @@ class RunBashCommandTool(ToolBase):
|
|
65
65
|
bufsize=1,
|
66
66
|
env=env,
|
67
67
|
)
|
68
|
-
stdout_lines = 0
|
69
|
-
stderr_lines = 0
|
70
|
-
stdout_content = []
|
71
|
-
stderr_content = []
|
72
|
-
max_lines = 100
|
73
|
-
import threading
|
74
|
-
|
75
|
-
def stream_reader(
|
76
|
-
stream, file_handle, report_func, content_list, line_counter
|
77
|
-
):
|
78
|
-
for line in iter(stream.readline, ""):
|
79
|
-
file_handle.write(line)
|
80
|
-
file_handle.flush()
|
81
|
-
report_func(line)
|
82
|
-
content_list.append(line)
|
83
|
-
line_counter[0] += 1
|
84
|
-
stream.close()
|
85
|
-
|
86
|
-
stdout_counter = [0]
|
87
|
-
stderr_counter = [0]
|
88
|
-
stdout_thread = threading.Thread(
|
89
|
-
target=stream_reader,
|
90
|
-
args=(
|
91
|
-
process.stdout,
|
92
|
-
stdout_file,
|
93
|
-
self.report_stdout,
|
94
|
-
stdout_content,
|
95
|
-
stdout_counter,
|
96
|
-
),
|
97
|
-
)
|
98
|
-
stderr_thread = threading.Thread(
|
99
|
-
target=stream_reader,
|
100
|
-
args=(
|
101
|
-
process.stderr,
|
102
|
-
stderr_file,
|
103
|
-
self.report_stderr,
|
104
|
-
stderr_content,
|
105
|
-
stderr_counter,
|
106
|
-
),
|
107
|
-
)
|
108
|
-
stdout_thread.start()
|
109
|
-
stderr_thread.start()
|
110
68
|
try:
|
111
|
-
process.
|
69
|
+
stdout_content, stderr_content = process.communicate(
|
70
|
+
timeout=timeout
|
71
|
+
)
|
112
72
|
except subprocess.TimeoutExpired:
|
113
73
|
process.kill()
|
114
74
|
self.report_error(
|
115
|
-
tr(
|
75
|
+
tr(
|
76
|
+
" \u274c Timed out after {timeout} seconds.",
|
77
|
+
timeout=timeout,
|
78
|
+
)
|
116
79
|
)
|
117
80
|
return tr(
|
118
81
|
"Command timed out after {timeout} seconds.", timeout=timeout
|
119
82
|
)
|
120
|
-
stdout_thread.join()
|
121
|
-
stderr_thread.join()
|
122
|
-
stdout_lines = stdout_counter[0]
|
123
|
-
stderr_lines = stderr_counter[0]
|
124
83
|
self.report_success(
|
125
|
-
tr(
|
84
|
+
tr(
|
85
|
+
" \u2705 return code {return_code}",
|
86
|
+
return_code=process.returncode,
|
87
|
+
)
|
126
88
|
)
|
127
89
|
warning_msg = ""
|
128
|
-
if
|
90
|
+
if requires_user_input:
|
129
91
|
warning_msg = tr(
|
130
|
-
"
|
92
|
+
"\u26a0\ufe0f Warning: This command might be interactive, require user input, and might hang.\n"
|
131
93
|
)
|
94
|
+
max_lines = 100
|
95
|
+
stdout_lines = stdout_content.count("\n")
|
96
|
+
stderr_lines = stderr_content.count("\n")
|
132
97
|
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
133
|
-
with open(
|
134
|
-
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
135
|
-
) as out_f:
|
136
|
-
stdout_content_str = out_f.read()
|
137
|
-
with open(
|
138
|
-
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
139
|
-
) as err_f:
|
140
|
-
stderr_content_str = err_f.read()
|
141
98
|
result = warning_msg + tr(
|
142
99
|
"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}",
|
143
100
|
return_code=process.returncode,
|
144
|
-
stdout_content=
|
101
|
+
stdout_content=stdout_content,
|
145
102
|
)
|
146
|
-
if
|
103
|
+
if stderr_content.strip():
|
147
104
|
result += tr(
|
148
105
|
"\n--- STDERR ---\n{stderr_content}",
|
149
|
-
stderr_content=
|
106
|
+
stderr_content=stderr_content,
|
150
107
|
)
|
151
108
|
return result
|
152
109
|
else:
|
@@ -167,5 +124,5 @@ class RunBashCommandTool(ToolBase):
|
|
167
124
|
)
|
168
125
|
return result
|
169
126
|
except Exception as e:
|
170
|
-
self.report_error(tr("
|
127
|
+
self.report_error(tr(" \u274c Error: {error}", error=e))
|
171
128
|
return tr("Error running command: {error}", error=e)
|
@@ -1,8 +1,10 @@
|
|
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
4
|
from janito.i18n import tr
|
4
5
|
import subprocess
|
5
6
|
import tempfile
|
7
|
+
import threading
|
6
8
|
|
7
9
|
|
8
10
|
@register_tool(name="run_powershell_command")
|
@@ -17,32 +19,16 @@ class RunPowerShellCommandTool(ToolBase):
|
|
17
19
|
command (str): The PowerShell command to execute. This string is passed directly to PowerShell using the --Command argument (not as a script file).
|
18
20
|
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
19
21
|
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
20
|
-
|
22
|
+
requires_user_input (bool, optional): If True, warns that the command may require user input and might hang. Defaults to False. Non-interactive commands are preferred for automation and reliability.
|
21
23
|
Returns:
|
22
24
|
str: Output and status message, or file paths/line counts if output is large.
|
23
25
|
"""
|
24
26
|
|
25
|
-
def
|
26
|
-
|
27
|
-
command: str,
|
28
|
-
timeout: int = 60,
|
29
|
-
require_confirmation: bool = False,
|
30
|
-
interactive: bool = False,
|
31
|
-
) -> str:
|
32
|
-
if not command.strip():
|
27
|
+
def _confirm_and_warn(self, command, require_confirmation, requires_user_input):
|
28
|
+
if requires_user_input:
|
33
29
|
self.report_warning(
|
34
|
-
tr("⚠️ Warning: Empty command provided. Operation skipped.")
|
35
|
-
)
|
36
|
-
return tr("Warning: Empty command provided. Operation skipped.")
|
37
|
-
encoding_prefix = "$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
|
38
|
-
command_with_encoding = encoding_prefix + command
|
39
|
-
self.report_info(
|
40
|
-
tr("🖥️ Running PowerShell command: {command} ...\n", command=command)
|
41
|
-
)
|
42
|
-
if interactive:
|
43
|
-
self.report_info(
|
44
30
|
tr(
|
45
|
-
"
|
31
|
+
"\u26a0\ufe0f Warning: This command might be interactive, require user input, and might hang."
|
46
32
|
)
|
47
33
|
)
|
48
34
|
if require_confirmation:
|
@@ -53,11 +39,109 @@ class RunPowerShellCommandTool(ToolBase):
|
|
53
39
|
)
|
54
40
|
)
|
55
41
|
if not confirmed:
|
56
|
-
self.report_warning(tr("Execution cancelled by user."))
|
57
|
-
return
|
58
|
-
|
42
|
+
self.report_warning(tr("\u26a0\ufe0f Execution cancelled by user."))
|
43
|
+
return False
|
44
|
+
return True
|
45
|
+
|
46
|
+
def _launch_process(self, shell_exe, command_with_encoding):
|
47
|
+
return subprocess.Popen(
|
48
|
+
[
|
49
|
+
shell_exe,
|
50
|
+
"-NoProfile",
|
51
|
+
"-ExecutionPolicy",
|
52
|
+
"Bypass",
|
53
|
+
"-Command",
|
54
|
+
command_with_encoding,
|
55
|
+
],
|
56
|
+
stdout=subprocess.PIPE,
|
57
|
+
stderr=subprocess.PIPE,
|
58
|
+
text=True,
|
59
|
+
bufsize=1,
|
60
|
+
universal_newlines=True,
|
61
|
+
encoding="utf-8",
|
62
|
+
)
|
63
|
+
|
64
|
+
def _stream_output(self, stream, file_obj, report_func, count_func, counter):
|
65
|
+
for line in stream:
|
66
|
+
file_obj.write(line)
|
67
|
+
file_obj.flush()
|
68
|
+
report_func(line)
|
69
|
+
if count_func == "stdout":
|
70
|
+
counter["stdout"] += 1
|
71
|
+
else:
|
72
|
+
counter["stderr"] += 1
|
73
|
+
|
74
|
+
def _format_result(
|
75
|
+
self, requires_user_input, return_code, stdout_file, stderr_file, max_lines=100
|
76
|
+
):
|
77
|
+
warning_msg = ""
|
78
|
+
if requires_user_input:
|
79
|
+
warning_msg = tr(
|
80
|
+
"\u26a0\ufe0f Warning: This command might be interactive, require user input, and might hang.\n"
|
81
|
+
)
|
82
|
+
with open(stdout_file.name, "r", encoding="utf-8", errors="replace") as out_f:
|
83
|
+
stdout_content = out_f.read()
|
84
|
+
with open(stderr_file.name, "r", encoding="utf-8", errors="replace") as err_f:
|
85
|
+
stderr_content = err_f.read()
|
86
|
+
stdout_lines = stdout_content.count("\n")
|
87
|
+
stderr_lines = stderr_content.count("\n")
|
88
|
+
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
89
|
+
result = warning_msg + tr(
|
90
|
+
"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}",
|
91
|
+
return_code=return_code,
|
92
|
+
stdout_content=stdout_content,
|
93
|
+
)
|
94
|
+
if stderr_content.strip():
|
95
|
+
result += tr(
|
96
|
+
"\n--- STDERR ---\n{stderr_content}",
|
97
|
+
stderr_content=stderr_content,
|
98
|
+
)
|
99
|
+
return result
|
100
|
+
else:
|
101
|
+
result = warning_msg + tr(
|
102
|
+
"stdout_file: {stdout_file} (lines: {stdout_lines})\n",
|
103
|
+
stdout_file=stdout_file.name,
|
104
|
+
stdout_lines=stdout_lines,
|
105
|
+
)
|
106
|
+
if stderr_lines > 0 and stderr_content.strip():
|
107
|
+
result += tr(
|
108
|
+
"stderr_file: {stderr_file} (lines: {stderr_lines})\n",
|
109
|
+
stderr_file=stderr_file.name,
|
110
|
+
stderr_lines=stderr_lines,
|
111
|
+
)
|
112
|
+
result += tr(
|
113
|
+
"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed.",
|
114
|
+
return_code=return_code,
|
115
|
+
)
|
116
|
+
return result
|
59
117
|
|
60
|
-
|
118
|
+
def run(
|
119
|
+
self,
|
120
|
+
command: str,
|
121
|
+
timeout: int = 60,
|
122
|
+
require_confirmation: bool = False,
|
123
|
+
requires_user_input: bool = False,
|
124
|
+
) -> str:
|
125
|
+
if not command.strip():
|
126
|
+
self.report_warning(tr("\u2139\ufe0f Empty command provided."))
|
127
|
+
return tr("Warning: Empty command provided. Operation skipped.")
|
128
|
+
encoding_prefix = "$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
|
129
|
+
command_with_encoding = encoding_prefix + command
|
130
|
+
self.report_info(
|
131
|
+
ActionType.EXECUTE,
|
132
|
+
tr(
|
133
|
+
"\U0001f5a5\ufe0f Running PowerShell command: {command} ...\n",
|
134
|
+
command=command,
|
135
|
+
),
|
136
|
+
)
|
137
|
+
if not self._confirm_and_warn(
|
138
|
+
command, require_confirmation, requires_user_input
|
139
|
+
):
|
140
|
+
return tr("\u274c Command execution cancelled by user.")
|
141
|
+
from janito.agent.platform_discovery import PlatformDiscovery
|
142
|
+
|
143
|
+
pd = PlatformDiscovery()
|
144
|
+
shell_exe = "powershell.exe" if pd.is_windows() else "pwsh"
|
61
145
|
try:
|
62
146
|
with (
|
63
147
|
tempfile.NamedTemporaryFile(
|
@@ -73,97 +157,53 @@ class RunPowerShellCommandTool(ToolBase):
|
|
73
157
|
encoding="utf-8",
|
74
158
|
) as stderr_file,
|
75
159
|
):
|
76
|
-
process =
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
160
|
+
process = self._launch_process(shell_exe, command_with_encoding)
|
161
|
+
counter = {"stdout": 0, "stderr": 0}
|
162
|
+
stdout_thread = threading.Thread(
|
163
|
+
target=self._stream_output,
|
164
|
+
args=(
|
165
|
+
process.stdout,
|
166
|
+
stdout_file,
|
167
|
+
self.report_stdout,
|
168
|
+
"stdout",
|
169
|
+
counter,
|
170
|
+
),
|
171
|
+
)
|
172
|
+
stderr_thread = threading.Thread(
|
173
|
+
target=self._stream_output,
|
174
|
+
args=(
|
175
|
+
process.stderr,
|
176
|
+
stderr_file,
|
177
|
+
self.report_stderr,
|
178
|
+
"stderr",
|
179
|
+
counter,
|
180
|
+
),
|
88
181
|
)
|
182
|
+
stdout_thread.start()
|
183
|
+
stderr_thread.start()
|
89
184
|
try:
|
90
185
|
return_code = process.wait(timeout=timeout)
|
91
186
|
except subprocess.TimeoutExpired:
|
92
187
|
process.kill()
|
93
188
|
self.report_error(
|
94
|
-
tr(
|
189
|
+
tr(
|
190
|
+
" \u274c Timed out after {timeout} seconds.",
|
191
|
+
timeout=timeout,
|
192
|
+
)
|
95
193
|
)
|
96
194
|
return tr(
|
97
195
|
"Command timed out after {timeout} seconds.", timeout=timeout
|
98
196
|
)
|
197
|
+
stdout_thread.join()
|
198
|
+
stderr_thread.join()
|
99
199
|
stdout_file.flush()
|
100
200
|
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)
|
121
201
|
self.report_success(
|
122
|
-
tr("
|
202
|
+
tr(" \u2705 return code {return_code}", return_code=return_code)
|
203
|
+
)
|
204
|
+
return self._format_result(
|
205
|
+
requires_user_input, return_code, stdout_file, stderr_file
|
123
206
|
)
|
124
|
-
warning_msg = ""
|
125
|
-
if interactive:
|
126
|
-
warning_msg = tr(
|
127
|
-
"⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
|
128
|
-
)
|
129
|
-
with open(
|
130
|
-
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
131
|
-
) as out_f:
|
132
|
-
stdout_content = out_f.read()
|
133
|
-
with open(
|
134
|
-
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
135
|
-
) as err_f:
|
136
|
-
stderr_content = err_f.read()
|
137
|
-
max_lines = 100
|
138
|
-
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
139
|
-
result = warning_msg + tr(
|
140
|
-
"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}",
|
141
|
-
return_code=return_code,
|
142
|
-
stdout_content=stdout_content,
|
143
|
-
)
|
144
|
-
if stderr_content.strip():
|
145
|
-
result += tr(
|
146
|
-
"\n--- STDERR ---\n{stderr_content}",
|
147
|
-
stderr_content=stderr_content,
|
148
|
-
)
|
149
|
-
return result
|
150
|
-
else:
|
151
|
-
result = warning_msg + tr(
|
152
|
-
"stdout_file: {stdout_file} (lines: {stdout_lines})\n",
|
153
|
-
stdout_file=stdout_file.name,
|
154
|
-
stdout_lines=stdout_lines,
|
155
|
-
)
|
156
|
-
if stderr_lines > 0 and stderr_content.strip():
|
157
|
-
result += tr(
|
158
|
-
"stderr_file: {stderr_file} (lines: {stderr_lines})\n",
|
159
|
-
stderr_file=stderr_file.name,
|
160
|
-
stderr_lines=stderr_lines,
|
161
|
-
)
|
162
|
-
result += tr(
|
163
|
-
"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed.",
|
164
|
-
return_code=return_code,
|
165
|
-
)
|
166
|
-
return result
|
167
207
|
except Exception as e:
|
168
|
-
self.report_error(tr("
|
208
|
+
self.report_error(tr(" \u274c Error: {error}", error=e))
|
169
209
|
return tr("Error running command: {error}", error=e)
|