janito 1.6.0__py3-none-any.whl → 1.7.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 (43) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config.py +2 -2
  3. janito/agent/config_defaults.py +1 -0
  4. janito/agent/conversation.py +4 -1
  5. janito/agent/openai_client.py +2 -0
  6. janito/agent/platform_discovery.py +90 -0
  7. janito/agent/profile_manager.py +83 -91
  8. janito/agent/rich_message_handler.py +72 -64
  9. janito/agent/templates/profiles/system_prompt_template_base.toml +76 -0
  10. janito/agent/templates/profiles/system_prompt_template_default.toml +3 -0
  11. janito/agent/templates/profiles/system_prompt_template_technical.toml +13 -0
  12. janito/agent/tests/test_prompt_toml.py +61 -0
  13. janito/agent/tools/__init__.py +4 -0
  14. janito/agent/tools/ask_user.py +8 -2
  15. janito/agent/tools/create_directory.py +27 -10
  16. janito/agent/tools/find_files.py +2 -10
  17. janito/agent/tools/get_file_outline.py +29 -0
  18. janito/agent/tools/get_lines.py +9 -10
  19. janito/agent/tools/memory.py +68 -0
  20. janito/agent/tools/run_bash_command.py +79 -60
  21. janito/agent/tools/run_powershell_command.py +153 -0
  22. janito/agent/tools/run_python_command.py +4 -0
  23. janito/agent/tools/search_files.py +0 -6
  24. janito/cli/_print_config.py +1 -1
  25. janito/cli/config_commands.py +1 -1
  26. janito/cli/main.py +1 -1
  27. janito/cli/runner/__init__.py +0 -2
  28. janito/cli/runner/cli_main.py +3 -13
  29. janito/cli/runner/config.py +4 -2
  30. janito/cli/runner/scan.py +22 -9
  31. janito/cli_chat_shell/chat_loop.py +13 -9
  32. janito/cli_chat_shell/chat_ui.py +2 -2
  33. janito/cli_chat_shell/commands/__init__.py +2 -0
  34. janito/cli_chat_shell/commands/sum.py +49 -0
  35. janito/cli_chat_shell/load_prompt.py +47 -8
  36. janito/cli_chat_shell/ui.py +8 -2
  37. janito/web/app.py +6 -9
  38. {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/METADATA +17 -9
  39. {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/RECORD +43 -35
  40. {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/WHEEL +0 -0
  41. {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/entry_points.txt +0 -0
  42. {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/licenses/LICENSE +0 -0
  43. {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,12 @@ from . import remove_file
13
13
  from . import replace_text_in_file
14
14
  from . import rich_live
15
15
  from . import run_bash_command
16
+ from . import run_powershell_command
16
17
  from . import run_python_command
17
18
  from . import search_files
18
19
  from . import tools_utils
19
20
  from . import replace_file
21
+ from . import memory
20
22
 
21
23
  __all__ = [
22
24
  "ask_user",
@@ -34,8 +36,10 @@ __all__ = [
34
36
  "replace_text_in_file",
35
37
  "rich_live",
36
38
  "run_bash_command",
39
+ "run_powershell_command",
37
40
  "run_python_command",
38
41
  "search_files",
39
42
  "tools_utils",
40
43
  "replace_file",
44
+ "memory",
41
45
  ]
@@ -34,12 +34,18 @@ class AskUserTool(ToolBase):
34
34
  def _(event):
35
35
  pass
36
36
 
37
+ # F12 instruction rotation
38
+ _f12_instructions = ["proceed", "go ahead", "continue", "next", "okay"]
39
+ _f12_index = {"value": 0}
40
+
37
41
  @bindings.add("f12")
38
42
  def _(event):
39
- """When F12 is pressed, send 'proceed' as input immediately."""
43
+ """When F12 is pressed, rotate through a set of short instructions."""
40
44
  buf = event.app.current_buffer
41
- buf.text = "proceed"
45
+ idx = _f12_index["value"]
46
+ buf.text = _f12_instructions[idx]
42
47
  buf.validate_and_handle()
48
+ _f12_index["value"] = (idx + 1) % len(_f12_instructions)
43
49
 
44
50
  style = Style.from_dict(
45
51
  {
@@ -1,6 +1,8 @@
1
1
  from janito.agent.tool_registry import register_tool
2
2
  from janito.agent.tools.utils import expand_path, display_path
3
3
  from janito.agent.tool_base import ToolBase
4
+ import os
5
+ import shutil
4
6
 
5
7
 
6
8
  @register_tool(name="create_directory")
@@ -21,13 +23,28 @@ class CreateDirectoryTool(ToolBase):
21
23
  original_path = path
22
24
  path = expand_path(path)
23
25
  disp_path = display_path(original_path, path)
24
- import os
25
-
26
- if os.path.exists(path):
27
- if not os.path.isdir(path):
28
- self.report_error(
29
- f"\u274c Path '{disp_path}' exists and is not a directory."
30
- )
31
- return f"\u274c Path '{disp_path}' exists and is not a directory."
32
- # Directory creation logic would go here
33
- return f"\u2705 Successfully created the directory at '{disp_path}'."
26
+ self.report_info(
27
+ f"\U0001f4c1 Creating directory: '{disp_path}' (overwrite={overwrite}) ... "
28
+ )
29
+ try:
30
+ if os.path.exists(path):
31
+ if not os.path.isdir(path):
32
+ self.report_error(
33
+ f"\u274c Path '{disp_path}' exists and is not a directory."
34
+ )
35
+ return f"\u274c Path '{disp_path}' exists and is not a directory."
36
+ if not overwrite:
37
+ self.report_error(
38
+ f"\u2757 Directory '{disp_path}' already exists (overwrite=False)"
39
+ )
40
+ return (
41
+ f"\u2757 Cannot create directory: '{disp_path}' already exists."
42
+ )
43
+ # Overwrite: remove existing directory
44
+ shutil.rmtree(path)
45
+ os.makedirs(path, exist_ok=True)
46
+ self.report_success(f"\u2705 Directory created at '{disp_path}'")
47
+ return f"\u2705 Successfully created the directory at '{disp_path}'."
48
+ except Exception as e:
49
+ self.report_error(f"\u274c Error creating directory '{disp_path}': {e}")
50
+ return f"\u274c Cannot create directory: {e}"
@@ -14,8 +14,7 @@ class FindFilesTool(ToolBase):
14
14
  Args:
15
15
  directories (list[str]): List of directories to search in.
16
16
  pattern (str): File pattern to match. Uses Unix shell-style wildcards (fnmatch), e.g. '*.py', 'data_??.csv', '[a-z]*.txt'.
17
- recursive (bool, optional): Whether to search recursively in subdirectories. Defaults to False.
18
- max_depth (int, optional): Maximum directory depth to search (0 = only top-level). If None, unlimited. Defaults to None.
17
+ recursive (bool, optional): Whether to search recursively in subdirectories. Defaults to True.
19
18
  Returns:
20
19
  str: Newline-separated list of matching file paths. Example:
21
20
  "/path/to/file1.py\n/path/to/file2.py"
@@ -26,8 +25,7 @@ class FindFilesTool(ToolBase):
26
25
  self,
27
26
  directories: list[str],
28
27
  pattern: str,
29
- recursive: bool = False,
30
- max_depth: int = None,
28
+ recursive: bool = True,
31
29
  ) -> str:
32
30
  import os
33
31
 
@@ -43,15 +41,9 @@ class FindFilesTool(ToolBase):
43
41
  disp_path = display_path(directory)
44
42
  self.report_info(f"🔍 Searching for files '{pattern}' in '{disp_path}'")
45
43
  for root, dirs, files in os.walk(directory):
46
- # Calculate depth
47
44
  rel_path = os.path.relpath(root, directory)
48
45
  depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
49
- if max_depth is not None and depth > max_depth:
50
- # Prune traversal
51
- dirs[:] = []
52
- continue
53
46
  if not recursive and depth > 0:
54
- # Only top-level if not recursive
55
47
  break
56
48
  dirs, files = filter_ignored(root, dirs, files)
57
49
  for filename in fnmatch.filter(files, pattern):
@@ -39,6 +39,12 @@ class GetFileOutlineTool(ToolBase):
39
39
  table = self._format_outline_table(outline_items)
40
40
  self.report_success(f"✅ {len(outline_items)} items ({outline_type})")
41
41
  return f"Outline: {len(outline_items)} items ({outline_type})\n" + table
42
+ elif ext == ".md":
43
+ outline_items = self._parse_markdown_outline(lines)
44
+ outline_type = "markdown"
45
+ table = self._format_markdown_outline_table(outline_items)
46
+ self.report_success(f"✅ {len(outline_items)} items ({outline_type})")
47
+ return f"Outline: {len(outline_items)} items ({outline_type})\n" + table
42
48
  else:
43
49
  outline_type = "default"
44
50
  self.report_success(f"✅ {len(lines)} lines ({outline_type})")
@@ -105,6 +111,29 @@ class GetFileOutlineTool(ToolBase):
105
111
  )
106
112
  return outline
107
113
 
114
+ def _parse_markdown_outline(self, lines: List[str]):
115
+ # Extract Markdown headers (e.g., #, ##, ###)
116
+ header_pat = re.compile(r"^(#+)\s+(.*)")
117
+ outline = []
118
+ for idx, line in enumerate(lines):
119
+ match = header_pat.match(line)
120
+ if match:
121
+ level = len(match.group(1))
122
+ title = match.group(2).strip()
123
+ outline.append({"level": level, "title": title, "line": idx + 1})
124
+ return outline
125
+
126
+ def _format_markdown_outline_table(self, outline_items):
127
+ if not outline_items:
128
+ return "No headers found."
129
+ header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
130
+ rows = []
131
+ for item in outline_items:
132
+ rows.append(
133
+ f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |"
134
+ )
135
+ return header + "\n" + "\n".join(rows)
136
+
108
137
  def _format_outline_table(self, outline_items):
109
138
  if not outline_items:
110
139
  return "No classes or functions found."
@@ -37,32 +37,31 @@ class GetLinesTool(ToolBase):
37
37
  ]
38
38
  selected_len = len(selected)
39
39
  total_lines = len(lines)
40
+ at_end = False
40
41
  if from_line and to_line:
41
42
  requested = to_line - from_line + 1
42
- if selected_len < requested:
43
+ if to_line >= total_lines or selected_len < requested:
44
+ at_end = True
45
+ if at_end:
43
46
  self.report_success(
44
- f" ✅ {selected_len} {pluralize('line', selected_len)} (end at line {total_lines})"
47
+ f" ✅ {selected_len} {pluralize('line', selected_len)} (end)"
45
48
  )
46
49
  elif to_line < total_lines:
47
50
  self.report_success(
48
51
  f" ✅ {selected_len} {pluralize('line', selected_len)} ({total_lines - to_line} lines to end)"
49
52
  )
50
- else:
51
- self.report_success(
52
- f" ✅ {selected_len} {pluralize('line', selected_len)} (end at line {total_lines})"
53
- )
54
53
  else:
55
54
  self.report_success(
56
55
  f" ✅ {selected_len} {pluralize('line', selected_len)} (full file)"
57
56
  )
58
57
  # Prepare header
59
58
  if from_line and to_line:
60
- header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (of {total_lines})\n---\n"
61
- if to_line >= total_lines:
62
- header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (end at line {total_lines})\n---\n"
59
+ if to_line >= total_lines or selected_len < (to_line - from_line + 1):
60
+ header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (end)\n---\n"
61
+ else:
62
+ header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (of {total_lines})\n---\n"
63
63
  elif from_line:
64
64
  header = f"---\nFile: {disp_path} | Lines: {from_line}-END (of {total_lines})\n---\n"
65
- header = f"---\nFile: {disp_path} | Lines: {from_line}-END (end at line {total_lines})\n---\n"
66
65
  else:
67
66
  header = (
68
67
  f"---\nFile: {disp_path} | All lines (total: {total_lines})\n---\n"
@@ -0,0 +1,68 @@
1
+ """
2
+ In-memory memory tools for storing and retrieving reusable information during an agent session.
3
+ These tools allow the agent to remember and recall arbitrary key-value pairs for the duration of the process.
4
+ """
5
+
6
+ from janito.agent.tool_base import ToolBase
7
+ from janito.agent.tool_registry import register_tool
8
+
9
+ # Simple in-memory store (process-local, not persistent)
10
+ _memory_store = {}
11
+
12
+
13
+ @register_tool(name="store_memory")
14
+ class StoreMemoryTool(ToolBase):
15
+ """
16
+ Store a value for later retrieval using a key. Use this tool to remember information that may be useful in future steps or requests.
17
+
18
+ Args:
19
+ key (str): The identifier for the value to store.
20
+ value (str): The value to store for later retrieval.
21
+ Returns:
22
+ str: Status message indicating success or error. Example:
23
+ - "✅ Stored value for key: 'foo'"
24
+ - "❗ Error storing value: ..."
25
+ """
26
+
27
+ def call(self, key: str, value: str) -> str:
28
+ self.report_info(f"Storing value for key: '{key}'")
29
+ try:
30
+ _memory_store[key] = value
31
+ msg = f"✅ Stored value for key: '{key}'"
32
+ self.report_success(msg)
33
+ return msg
34
+ except Exception as e:
35
+ msg = f"❗ Error storing value: {e}"
36
+ self.report_error(msg)
37
+ return msg
38
+
39
+
40
+ @register_tool(name="retrieve_memory")
41
+ class RetrieveMemoryTool(ToolBase):
42
+ """
43
+ Retrieve a value previously stored using a key. Use this tool to recall information remembered earlier in the session.
44
+
45
+ Args:
46
+ key (str): The identifier for the value to retrieve.
47
+ Returns:
48
+ str: The stored value, or a warning message if not found. Example:
49
+ - "🔎 Retrieved value for key: 'foo': bar"
50
+ - "⚠️ No value found for key: 'notfound'"
51
+ """
52
+
53
+ def call(self, key: str) -> str:
54
+ self.report_info(f"Retrieving value for key: '{key}'")
55
+ try:
56
+ if key in _memory_store:
57
+ value = _memory_store[key]
58
+ msg = f"🔎 Retrieved value for key: '{key}': {value}"
59
+ self.report_success(msg)
60
+ return msg
61
+ else:
62
+ msg = f"⚠️ No value found for key: '{key}'"
63
+ self.report_warning(msg)
64
+ return msg
65
+ except Exception as e:
66
+ msg = f"❗ Error retrieving value: {e}"
67
+ self.report_error(msg)
68
+ return msg
@@ -4,6 +4,7 @@ from janito.agent.tool_registry import register_tool
4
4
  import subprocess
5
5
  import tempfile
6
6
  import sys
7
+ import os
7
8
 
8
9
 
9
10
  @register_tool(name="run_bash_command")
@@ -33,18 +34,6 @@ class RunBashCommandTool(ToolBase):
33
34
  """
34
35
  Execute a bash command and capture live output.
35
36
 
36
- Args:
37
- command (str): The bash command to execute.
38
- timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
39
- require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
40
- interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
41
-
42
- Returns:
43
- str: Output and status message.
44
- """
45
- """
46
- Execute a bash command and capture live output.
47
-
48
37
  Args:
49
38
  command (str): The bash command to execute.
50
39
  timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
@@ -62,7 +51,6 @@ class RunBashCommandTool(ToolBase):
62
51
  self.report_info(
63
52
  "⚠️ Warning: This command might be interactive, require user input, and might hang."
64
53
  )
65
-
66
54
  sys.stdout.flush()
67
55
 
68
56
  try:
@@ -74,81 +62,112 @@ class RunBashCommandTool(ToolBase):
74
62
  mode="w+", prefix="run_bash_stderr_", delete=False, encoding="utf-8"
75
63
  ) as stderr_file,
76
64
  ):
77
- # Use bash explicitly for command execution
65
+ env = os.environ.copy()
66
+ env["PYTHONIOENCODING"] = "utf-8"
67
+ env["LC_ALL"] = "C.UTF-8"
68
+ env["LANG"] = "C.UTF-8"
69
+
78
70
  process = subprocess.Popen(
79
71
  ["bash", "-c", command],
80
- stdout=stdout_file,
81
- stderr=stderr_file,
72
+ stdout=subprocess.PIPE,
73
+ stderr=subprocess.PIPE,
82
74
  text=True,
75
+ encoding="utf-8",
76
+ bufsize=1, # line-buffered
77
+ env=env,
83
78
  )
79
+
80
+ stdout_lines = 0
81
+ stderr_lines = 0
82
+ stdout_content = []
83
+ stderr_content = []
84
+ max_lines = 100
85
+
86
+ import threading
87
+
88
+ def stream_reader(
89
+ stream, file_handle, report_func, content_list, line_counter
90
+ ):
91
+ for line in iter(stream.readline, ""):
92
+ file_handle.write(line)
93
+ file_handle.flush()
94
+ report_func(line)
95
+ content_list.append(line)
96
+ line_counter[0] += 1
97
+ stream.close()
98
+
99
+ stdout_counter = [0]
100
+ stderr_counter = [0]
101
+ stdout_thread = threading.Thread(
102
+ target=stream_reader,
103
+ args=(
104
+ process.stdout,
105
+ stdout_file,
106
+ self.report_stdout,
107
+ stdout_content,
108
+ stdout_counter,
109
+ ),
110
+ )
111
+ stderr_thread = threading.Thread(
112
+ target=stream_reader,
113
+ args=(
114
+ process.stderr,
115
+ stderr_file,
116
+ self.report_stderr,
117
+ stderr_content,
118
+ stderr_counter,
119
+ ),
120
+ )
121
+ stdout_thread.start()
122
+ stderr_thread.start()
123
+
84
124
  try:
85
- return_code = process.wait(timeout=timeout)
125
+ process.wait(timeout=timeout)
86
126
  except subprocess.TimeoutExpired:
87
127
  process.kill()
88
128
  self.report_error(f" ❌ Timed out after {timeout} seconds.")
89
129
  return f"Command timed out after {timeout} seconds."
90
130
 
91
- # Print live output to user
92
- stdout_file.flush()
93
- stderr_file.flush()
94
- with open(
95
- stdout_file.name, "r", encoding="utf-8", errors="replace"
96
- ) as out_f:
97
- out_f.seek(0)
98
- for line in out_f:
99
- self.report_stdout(line)
100
- with open(
101
- stderr_file.name, "r", encoding="utf-8", errors="replace"
102
- ) as err_f:
103
- err_f.seek(0)
104
- for line in err_f:
105
- self.report_stderr(line)
131
+ stdout_thread.join()
132
+ stderr_thread.join()
106
133
 
107
134
  # Count lines
108
- with open(
109
- stdout_file.name, "r", encoding="utf-8", errors="replace"
110
- ) as out_f:
111
- stdout_lines = sum(1 for _ in out_f)
112
- with open(
113
- stderr_file.name, "r", encoding="utf-8", errors="replace"
114
- ) as err_f:
115
- stderr_lines = sum(1 for _ in err_f)
116
-
117
- self.report_success(f" ✅ return code {return_code}")
135
+ stdout_lines = stdout_counter[0]
136
+ stderr_lines = stderr_counter[0]
137
+
138
+ self.report_success(f" return code {process.returncode}")
118
139
  warning_msg = ""
119
140
  if interactive:
120
141
  warning_msg = "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
121
142
 
122
- # Read output contents
123
- with open(
124
- stdout_file.name, "r", encoding="utf-8", errors="replace"
125
- ) as out_f:
126
- stdout_content = out_f.read()
127
- with open(
128
- stderr_file.name, "r", encoding="utf-8", errors="replace"
129
- ) as err_f:
130
- stderr_content = err_f.read()
131
-
132
- # Thresholds
133
- max_lines = 100
143
+ # Read output contents if small
134
144
  if stdout_lines <= max_lines and stderr_lines <= max_lines:
145
+ # Read files from disk to ensure all content is included
146
+ with open(
147
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
148
+ ) as out_f:
149
+ stdout_content_str = out_f.read()
150
+ with open(
151
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
152
+ ) as err_f:
153
+ stderr_content_str = err_f.read()
135
154
  result = (
136
155
  warning_msg
137
- + f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
156
+ + f"Return code: {process.returncode}\n--- STDOUT ---\n{stdout_content_str}"
138
157
  )
139
- if stderr_content.strip():
140
- result += f"\n--- STDERR ---\n{stderr_content}"
158
+ if stderr_content_str.strip():
159
+ result += f"\n--- STDERR ---\n{stderr_content_str}"
141
160
  return result
142
161
  else:
143
162
  result = (
144
163
  warning_msg
145
- + f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
164
+ + f"[LARGE OUTPUT]\nstdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
146
165
  )
147
- if stderr_lines > 0 and stderr_content.strip():
166
+ if stderr_lines > 0:
148
167
  result += (
149
168
  f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
150
169
  )
151
- result += f"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed."
170
+ result += f"returncode: {process.returncode}\nUse the get_lines tool to inspect the contents of these files when needed."
152
171
  return result
153
172
  except Exception as e:
154
173
  self.report_error(f" ❌ Error: {e}")
@@ -0,0 +1,153 @@
1
+ from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tool_registry import register_tool
3
+
4
+ import subprocess
5
+ import tempfile
6
+
7
+
8
+ @register_tool(name="run_powershell_command")
9
+ class RunPowerShellCommandTool(ToolBase):
10
+ """
11
+ Execute a non-interactive command using the PowerShell shell and capture live output.
12
+
13
+ This tool explicitly invokes 'powershell.exe' (on Windows) or 'pwsh' (on other platforms if available).
14
+
15
+ All commands are automatically prepended with UTF-8 output encoding:
16
+ $OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8;
17
+
18
+ For file output, it is recommended to use -Encoding utf8 in your PowerShell commands (e.g., Out-File -Encoding utf8) to ensure correct file encoding.
19
+
20
+ Args:
21
+ command (str): The PowerShell command to execute. This string is passed directly to PowerShell using the --Command argument (not as a script file).
22
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
23
+ require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
24
+ 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.
25
+
26
+ Returns:
27
+ str: Output and status message, or file paths/line counts if output is large.
28
+ """
29
+
30
+ def call(
31
+ self,
32
+ command: str,
33
+ timeout: int = 60,
34
+ require_confirmation: bool = False,
35
+ interactive: bool = False,
36
+ ) -> str:
37
+ if not command.strip():
38
+ self.report_warning("⚠️ Warning: Empty command provided. Operation skipped.")
39
+ return "Warning: Empty command provided. Operation skipped."
40
+ # Prepend UTF-8 output encoding
41
+ encoding_prefix = "$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
42
+ command_with_encoding = encoding_prefix + command
43
+ self.report_info(f"🖥️ Running PowerShell command: {command}\n")
44
+ if interactive:
45
+ self.report_info(
46
+ "⚠️ Warning: This command might be interactive, require user input, and might hang."
47
+ )
48
+ if require_confirmation:
49
+ confirmed = self.ask_user_confirmation(
50
+ f"About to run PowerShell command: {command}\nContinue?"
51
+ )
52
+ if not confirmed:
53
+ self.report_warning("Execution cancelled by user.")
54
+ return "❌ Command execution cancelled by user."
55
+ from janito.agent.platform_discovery import is_windows
56
+
57
+ shell_exe = "powershell.exe" if is_windows() else "pwsh"
58
+ try:
59
+ with (
60
+ tempfile.NamedTemporaryFile(
61
+ mode="w+",
62
+ prefix="run_powershell_stdout_",
63
+ delete=False,
64
+ encoding="utf-8",
65
+ ) as stdout_file,
66
+ tempfile.NamedTemporaryFile(
67
+ mode="w+",
68
+ prefix="run_powershell_stderr_",
69
+ delete=False,
70
+ encoding="utf-8",
71
+ ) as stderr_file,
72
+ ):
73
+ process = subprocess.Popen(
74
+ [
75
+ shell_exe,
76
+ "-NoProfile",
77
+ "-ExecutionPolicy",
78
+ "Bypass",
79
+ "-Command",
80
+ command_with_encoding,
81
+ ],
82
+ stdout=stdout_file,
83
+ stderr=stderr_file,
84
+ text=True,
85
+ )
86
+ try:
87
+ return_code = process.wait(timeout=timeout)
88
+ except subprocess.TimeoutExpired:
89
+ process.kill()
90
+ self.report_error(f" ❌ Timed out after {timeout} seconds.")
91
+ return f"Command timed out after {timeout} seconds."
92
+ # Print live output to user
93
+ stdout_file.flush()
94
+ stderr_file.flush()
95
+ with open(
96
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
97
+ ) as out_f:
98
+ out_f.seek(0)
99
+ for line in out_f:
100
+ self.report_stdout(line)
101
+ with open(
102
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
103
+ ) as err_f:
104
+ err_f.seek(0)
105
+ for line in err_f:
106
+ self.report_stderr(line)
107
+ # Count lines
108
+ with open(
109
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
110
+ ) as out_f:
111
+ stdout_lines = sum(1 for _ in out_f)
112
+ with open(
113
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
114
+ ) as err_f:
115
+ stderr_lines = sum(1 for _ in err_f)
116
+ self.report_success(f" ✅ return code {return_code}")
117
+ warning_msg = ""
118
+ if interactive:
119
+ warning_msg = "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
120
+ # Read output contents
121
+ with open(
122
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
123
+ ) as out_f:
124
+ stdout_content = out_f.read()
125
+ with open(
126
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
127
+ ) as err_f:
128
+ stderr_content = err_f.read()
129
+ # Thresholds
130
+ max_lines = 100
131
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
132
+ result = (
133
+ warning_msg
134
+ + f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
135
+ )
136
+ if stderr_content.strip():
137
+ result += f"\n--- STDERR ---\n{stderr_content}"
138
+ return result
139
+ else:
140
+ result = (
141
+ warning_msg
142
+ + f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
143
+ )
144
+ if stderr_lines > 0 and stderr_content.strip():
145
+ result += (
146
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
147
+ )
148
+ result += f"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed."
149
+ return result
150
+ except Exception as e:
151
+ self.report_error(f" ❌ Error: {e}")
152
+ return f"Error running command: {e}"
153
+ # No temp script file to clean up anymore
@@ -1,6 +1,7 @@
1
1
  import subprocess
2
2
  import tempfile
3
3
  import sys
4
+ import os
4
5
  from janito.agent.tool_base import ToolBase
5
6
  from janito.agent.tool_registry import register_tool
6
7
 
@@ -64,11 +65,14 @@ class RunPythonCommandTool(ToolBase):
64
65
  ):
65
66
  code_file.write(code)
66
67
  code_file.flush()
68
+ env = os.environ.copy()
69
+ env["PYTHONIOENCODING"] = "utf-8"
67
70
  process = subprocess.Popen(
68
71
  [sys.executable, code_file.name],
69
72
  stdout=stdout_file,
70
73
  stderr=stderr_file,
71
74
  text=True,
75
+ env=env,
72
76
  )
73
77
  try:
74
78
  return_code = process.wait(timeout=timeout)