janito 1.8.1__py3-none-any.whl → 1.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,11 @@
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
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 = expand_path(file_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
- 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)
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
- 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)
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.tools.tools_utils import display_path
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 "(unique)"
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
- info_msg + (" ..." if not info_msg.rstrip().endswith("...") else "")
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("⚠️ Search text is not unique."))
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
- interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False. Non-interactive commands are preferred for automation and reliability.
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
- interactive: bool = False,
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
- tr("🖥️ Running bash command: {command} ...\n", command=command)
36
+ ActionType.EXECUTE,
37
+ tr("🖥️ Running bash command: {command} ...\n", command=command),
38
38
  )
39
- if interactive:
40
- self.report_info(
39
+ if requires_user_input:
40
+ self.report_warning(
41
41
  tr(
42
- "⚠️ Warning: This command might be interactive, require user input, and might hang."
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.wait(timeout=timeout)
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(" ❌ Timed out after {timeout} seconds.", timeout=timeout)
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(" ✅ return code {return_code}", return_code=process.returncode)
84
+ tr(
85
+ " \u2705 return code {return_code}",
86
+ return_code=process.returncode,
87
+ )
126
88
  )
127
89
  warning_msg = ""
128
- if interactive:
90
+ if requires_user_input:
129
91
  warning_msg = tr(
130
- "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
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=stdout_content_str,
101
+ stdout_content=stdout_content,
145
102
  )
146
- if stderr_content_str.strip():
103
+ if stderr_content.strip():
147
104
  result += tr(
148
105
  "\n--- STDERR ---\n{stderr_content}",
149
- stderr_content=stderr_content_str,
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(" Error: {error}", error=e))
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
- interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False. Non-interactive commands are preferred for automation and reliability.
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 run(
26
- self,
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
- "⚠️ Warning: This command might be interactive, require user input, and might hang."
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 tr("❌ Command execution cancelled by user.")
58
- from janito.agent.platform_discovery import is_windows
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
- shell_exe = "powershell.exe" if is_windows() else "pwsh"
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 = subprocess.Popen(
77
- [
78
- shell_exe,
79
- "-NoProfile",
80
- "-ExecutionPolicy",
81
- "Bypass",
82
- "-Command",
83
- command_with_encoding,
84
- ],
85
- stdout=stdout_file,
86
- stderr=stderr_file,
87
- text=True,
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(" ❌ Timed out after {timeout} seconds.", timeout=timeout)
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(" return code {return_code}", return_code=return_code)
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(" Error: {error}", error=e))
208
+ self.report_error(tr(" \u274c Error: {error}", error=e))
169
209
  return tr("Error running command: {error}", error=e)