janito 1.9.0__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 (81) 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 -26
  5. janito/agent/conversation.py +163 -122
  6. janito/agent/conversation_api.py +149 -159
  7. janito/agent/{conversation_history.py → llm_conversation_history.py} +18 -1
  8. janito/agent/openai_client.py +38 -23
  9. janito/agent/openai_schema_generator.py +162 -129
  10. janito/agent/platform_discovery.py +134 -77
  11. janito/agent/profile_manager.py +5 -5
  12. janito/agent/rich_message_handler.py +80 -31
  13. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +5 -4
  14. janito/agent/test_openai_schema_generator.py +93 -0
  15. janito/agent/tool_base.py +7 -2
  16. janito/agent/tool_executor.py +54 -49
  17. janito/agent/tool_registry.py +5 -2
  18. janito/agent/tool_use_tracker.py +26 -5
  19. janito/agent/tools/__init__.py +6 -3
  20. janito/agent/tools/create_directory.py +3 -1
  21. janito/agent/tools/create_file.py +7 -1
  22. janito/agent/tools/fetch_url.py +40 -3
  23. janito/agent/tools/find_files.py +3 -1
  24. janito/agent/tools/get_file_outline/core.py +6 -7
  25. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  26. janito/agent/tools/get_lines.py +7 -2
  27. janito/agent/tools/move_file.py +3 -1
  28. janito/agent/tools/present_choices.py +3 -1
  29. janito/agent/tools/python_command_runner.py +150 -0
  30. janito/agent/tools/python_file_runner.py +148 -0
  31. janito/agent/tools/python_stdin_runner.py +154 -0
  32. janito/agent/tools/remove_directory.py +3 -1
  33. janito/agent/tools/remove_file.py +5 -1
  34. janito/agent/tools/replace_file.py +12 -2
  35. janito/agent/tools/replace_text_in_file.py +4 -2
  36. janito/agent/tools/run_bash_command.py +30 -69
  37. janito/agent/tools/run_powershell_command.py +134 -105
  38. janito/agent/tools/search_text.py +172 -122
  39. janito/agent/tools/validate_file_syntax/core.py +3 -1
  40. janito/agent/tools_utils/action_type.py +7 -0
  41. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  42. janito/agent/tools_utils/formatting.py +47 -21
  43. janito/agent/tools_utils/gitignore_utils.py +66 -40
  44. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  45. janito/cli/_print_config.py +63 -61
  46. janito/cli/arg_parser.py +13 -12
  47. janito/cli/cli_main.py +137 -147
  48. janito/cli/main.py +152 -174
  49. janito/cli/one_shot.py +40 -26
  50. janito/i18n/__init__.py +1 -1
  51. janito/rich_utils.py +46 -8
  52. janito/shell/commands/__init__.py +2 -4
  53. janito/shell/commands/conversation_restart.py +3 -1
  54. janito/shell/commands/edit.py +3 -0
  55. janito/shell/commands/history_view.py +3 -3
  56. janito/shell/commands/lang.py +3 -0
  57. janito/shell/commands/livelogs.py +5 -3
  58. janito/shell/commands/prompt.py +6 -0
  59. janito/shell/commands/session.py +3 -0
  60. janito/shell/commands/session_control.py +3 -0
  61. janito/shell/commands/termweb_log.py +8 -0
  62. janito/shell/commands/tools.py +3 -0
  63. janito/shell/commands/track.py +36 -0
  64. janito/shell/commands/utility.py +13 -18
  65. janito/shell/commands/verbose.py +3 -4
  66. janito/shell/input_history.py +62 -0
  67. janito/shell/main.py +117 -181
  68. janito/shell/session/manager.py +0 -21
  69. janito/shell/ui/interactive.py +0 -2
  70. janito/termweb/static/editor.css +0 -4
  71. janito/tests/test_rich_utils.py +44 -0
  72. janito/web/app.py +0 -75
  73. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/METADATA +61 -42
  74. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/RECORD +78 -71
  75. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  76. janito/agent/providers.py +0 -77
  77. janito/agent/tools/run_python_command.py +0 -161
  78. janito/shell/commands/sum.py +0 -49
  79. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  80. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  81. {janito-1.9.0.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,10 @@
1
1
  from janito.agent.tool_registry import register_tool
2
2
  from .python_outline import parse_python_outline
3
3
  from .markdown_outline import parse_markdown_outline
4
- from janito.agent.tools_utils.formatting import (
5
- format_outline_table,
6
- format_markdown_outline_table,
7
- )
4
+ from janito.agent.tools_utils.formatting import OutlineFormatter
8
5
  import os
9
6
  from janito.agent.tool_base import ToolBase
7
+ from janito.agent.tools_utils.action_type import ActionType
10
8
  from janito.agent.tools_utils.utils import display_path, pluralize
11
9
  from janito.i18n import tr
12
10
 
@@ -23,10 +21,11 @@ class GetFileOutlineTool(ToolBase):
23
21
  def run(self, file_path: str) -> str:
24
22
  try:
25
23
  self.report_info(
24
+ ActionType.READ,
26
25
  tr(
27
26
  "📄 Outlining file '{disp_path}' ...",
28
27
  disp_path=display_path(file_path),
29
- )
28
+ ),
30
29
  )
31
30
  ext = os.path.splitext(file_path)[1].lower()
32
31
  with open(file_path, "r", encoding="utf-8", errors="replace") as f:
@@ -34,7 +33,7 @@ class GetFileOutlineTool(ToolBase):
34
33
  if ext == ".py":
35
34
  outline_items = parse_python_outline(lines)
36
35
  outline_type = "python"
37
- table = format_outline_table(outline_items)
36
+ table = OutlineFormatter.format_outline_table(outline_items)
38
37
  self.report_success(
39
38
  tr(
40
39
  "✅ Outlined {count} {item_word}",
@@ -53,7 +52,7 @@ class GetFileOutlineTool(ToolBase):
53
52
  elif ext == ".md":
54
53
  outline_items = parse_markdown_outline(lines)
55
54
  outline_type = "markdown"
56
- table = format_markdown_outline_table(outline_items)
55
+ table = OutlineFormatter.format_markdown_outline_table(outline_items)
57
56
  self.report_success(
58
57
  tr(
59
58
  "✅ Outlined {count} {item_word}",
@@ -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
 
4
5
 
@@ -13,10 +14,11 @@ class SearchOutlineTool(ToolBase):
13
14
  from janito.i18n import tr
14
15
 
15
16
  self.report_info(
17
+ ActionType.READ,
16
18
  tr(
17
19
  "🔍 Searching for outline in '{disp_path}'",
18
20
  disp_path=display_path(file_path),
19
- )
21
+ ),
20
22
  )
21
23
  # ... rest of implementation ...
22
24
  # Example warnings and successes:
@@ -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.agent.tools_utils.utils import pluralize
4
5
  from janito.i18n import tr
@@ -30,15 +31,19 @@ class GetLinesTool(ToolBase):
30
31
  disp_path = display_path(file_path)
31
32
  if from_line and to_line:
32
33
  self.report_info(
34
+ ActionType.READ,
33
35
  tr(
34
36
  "📖 Reading file '{disp_path}' {from_line}-{to_line}",
35
37
  disp_path=disp_path,
36
38
  from_line=from_line,
37
39
  to_line=to_line,
38
- )
40
+ ),
39
41
  )
40
42
  else:
41
- self.report_info(tr("📖 Reading file '{disp_path}'", disp_path=disp_path))
43
+ self.report_info(
44
+ ActionType.READ,
45
+ tr("📖 Reading file '{disp_path}'", disp_path=disp_path),
46
+ )
42
47
  try:
43
48
  with open(file_path, "r", encoding="utf-8", errors="replace") as f:
44
49
  lines = f.readlines()
@@ -5,6 +5,7 @@ from janito.agent.tool_registry import register_tool
5
5
  # from janito.agent.tools_utils.expand_path import expand_path
6
6
  from janito.agent.tools_utils.utils import display_path
7
7
  from janito.agent.tool_base import ToolBase
8
+ from janito.agent.tools_utils.action_type import ActionType
8
9
  from janito.i18n import tr
9
10
 
10
11
 
@@ -85,11 +86,12 @@ class MoveFileTool(ToolBase):
85
86
  return tr("❌ Error removing destination before move: {error}", error=e)
86
87
  try:
87
88
  self.report_info(
89
+ ActionType.WRITE,
88
90
  tr(
89
91
  "📝 Moving from '{disp_src}' to '{disp_dest}' ...",
90
92
  disp_src=disp_src,
91
93
  disp_dest=disp_dest,
92
- )
94
+ ),
93
95
  )
94
96
  shutil.move(src, dest)
95
97
  self.report_success(tr("✅ Move complete."))
@@ -1,5 +1,6 @@
1
1
  from typing import List
2
2
  from janito.agent.tool_base import ToolBase
3
+ from janito.agent.tools_utils.action_type import ActionType
3
4
  from janito.agent.tool_registry import register_tool
4
5
  from janito.i18n import tr
5
6
  import questionary
@@ -36,11 +37,12 @@ class PresentChoicesTool(ToolBase):
36
37
  if not choices:
37
38
  return tr("⚠️ No choices provided.")
38
39
  self.report_info(
40
+ ActionType.EXECUTE,
39
41
  tr(
40
42
  "ℹ️ Prompting user: {prompt} (multi_select={multi_select}) ...",
41
43
  prompt=prompt,
42
44
  multi_select=multi_select,
43
- )
45
+ ),
44
46
  )
45
47
  if multi_select:
46
48
  result = questionary.checkbox(
@@ -0,0 +1,150 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ import threading
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
7
+ from janito.agent.tool_registry import register_tool
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="python_command_runner")
12
+ class PythonCommandRunnerTool(ToolBase):
13
+ """
14
+ Tool to execute Python code using the `python -c` command-line flag.
15
+ Args:
16
+ code (str): The Python code to execute as a string.
17
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
+ Returns:
19
+ str: Output and status message, or file paths/line counts if output is large.
20
+ """
21
+
22
+ def run(self, code: str, timeout: int = 60) -> str:
23
+ if not code.strip():
24
+ self.report_warning(tr("\u2139\ufe0f Empty code provided."))
25
+ return tr("Warning: Empty code provided. Operation skipped.")
26
+ self.report_info(
27
+ ActionType.EXECUTE, tr("🐍 Running: python -c ...\n{code}\n", code=code)
28
+ )
29
+ try:
30
+ with (
31
+ tempfile.NamedTemporaryFile(
32
+ mode="w+",
33
+ prefix="python_cmd_stdout_",
34
+ delete=False,
35
+ encoding="utf-8",
36
+ ) as stdout_file,
37
+ tempfile.NamedTemporaryFile(
38
+ mode="w+",
39
+ prefix="python_cmd_stderr_",
40
+ delete=False,
41
+ encoding="utf-8",
42
+ ) as stderr_file,
43
+ ):
44
+ process = subprocess.Popen(
45
+ [sys.executable, "-c", code],
46
+ stdout=subprocess.PIPE,
47
+ stderr=subprocess.PIPE,
48
+ text=True,
49
+ bufsize=1,
50
+ universal_newlines=True,
51
+ encoding="utf-8",
52
+ env={**dict(), **dict(PYTHONIOENCODING="utf-8")},
53
+ )
54
+ stdout_lines = 0
55
+ stderr_lines = 0
56
+
57
+ def stream_output(stream, file_obj, report_func, count_func):
58
+ nonlocal stdout_lines, stderr_lines
59
+ for line in stream:
60
+ file_obj.write(line)
61
+ file_obj.flush()
62
+ report_func(line)
63
+ if count_func == "stdout":
64
+ stdout_lines += 1
65
+ else:
66
+ stderr_lines += 1
67
+
68
+ stdout_thread = threading.Thread(
69
+ target=stream_output,
70
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
71
+ )
72
+ stderr_thread = threading.Thread(
73
+ target=stream_output,
74
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
75
+ )
76
+ stdout_thread.start()
77
+ stderr_thread.start()
78
+ try:
79
+ return_code = process.wait(timeout=timeout)
80
+ except subprocess.TimeoutExpired:
81
+ process.kill()
82
+ self.report_error(
83
+ tr("\u274c Timed out after {timeout} seconds.", timeout=timeout)
84
+ )
85
+ return tr(
86
+ "Code timed out after {timeout} seconds.", timeout=timeout
87
+ )
88
+ stdout_thread.join()
89
+ stderr_thread.join()
90
+ stdout_file.flush()
91
+ stderr_file.flush()
92
+
93
+ self.report_success(
94
+ tr("\u2705 Return code {return_code}", return_code=return_code)
95
+ )
96
+ # Read back the content for summary if not too large
97
+ with open(
98
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
99
+ ) as out_f:
100
+ stdout_content = out_f.read()
101
+ with open(
102
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
103
+ ) as err_f:
104
+ stderr_content = err_f.read()
105
+ max_lines = 100
106
+ stdout_lines = stdout_content.count("\n")
107
+ stderr_lines = stderr_content.count("\n")
108
+
109
+ def head_tail(text, n=10):
110
+ lines = text.splitlines()
111
+ if len(lines) <= 2 * n:
112
+ return "\n".join(lines)
113
+ return "\n".join(
114
+ lines[:n]
115
+ + ["... ({} lines omitted) ...".format(len(lines) - 2 * n)]
116
+ + lines[-n:]
117
+ )
118
+
119
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
120
+ result = (
121
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
122
+ )
123
+ if stderr_content.strip():
124
+ result += f"\n--- STDERR ---\n{stderr_content}"
125
+ return result
126
+ else:
127
+ result = (
128
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
129
+ )
130
+ if stderr_lines > 0 and stderr_content.strip():
131
+ result += (
132
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
133
+ )
134
+ result += f"returncode: {return_code}\n"
135
+ result += (
136
+ "--- STDOUT (head/tail) ---\n"
137
+ + head_tail(stdout_content)
138
+ + "\n"
139
+ )
140
+ if stderr_content.strip():
141
+ result += (
142
+ "--- STDERR (head/tail) ---\n"
143
+ + head_tail(stderr_content)
144
+ + "\n"
145
+ )
146
+ result += "Use the get_lines tool to inspect the contents of these files when needed."
147
+ return result
148
+ except Exception as e:
149
+ self.report_error(tr("\u274c Error: {error}", error=e))
150
+ return tr("Error running code: {error}", error=e)
@@ -0,0 +1,148 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ import threading
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
7
+ from janito.agent.tool_registry import register_tool
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="python_file_runner")
12
+ class PythonFileRunnerTool(ToolBase):
13
+ """
14
+ Tool to execute a specified Python script file.
15
+ Args:
16
+ file_path (str): Path to the Python script file to execute.
17
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
+ Returns:
19
+ str: Output and status message, or file paths/line counts if output is large.
20
+ """
21
+
22
+ def run(self, file_path: str, timeout: int = 60) -> str:
23
+ self.report_info(
24
+ ActionType.EXECUTE,
25
+ tr("🚀 Running: python {file_path}", file_path=file_path),
26
+ )
27
+ try:
28
+ with (
29
+ tempfile.NamedTemporaryFile(
30
+ mode="w+",
31
+ prefix="python_file_stdout_",
32
+ delete=False,
33
+ encoding="utf-8",
34
+ ) as stdout_file,
35
+ tempfile.NamedTemporaryFile(
36
+ mode="w+",
37
+ prefix="python_file_stderr_",
38
+ delete=False,
39
+ encoding="utf-8",
40
+ ) as stderr_file,
41
+ ):
42
+ process = subprocess.Popen(
43
+ [sys.executable, file_path],
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.PIPE,
46
+ text=True,
47
+ bufsize=1,
48
+ universal_newlines=True,
49
+ encoding="utf-8",
50
+ env={**dict(), **dict(PYTHONIOENCODING="utf-8")},
51
+ )
52
+ stdout_lines = 0
53
+ stderr_lines = 0
54
+
55
+ def stream_output(stream, file_obj, report_func, count_func):
56
+ nonlocal stdout_lines, stderr_lines
57
+ for line in stream:
58
+ file_obj.write(line)
59
+ file_obj.flush()
60
+ report_func(line)
61
+ if count_func == "stdout":
62
+ stdout_lines += 1
63
+ else:
64
+ stderr_lines += 1
65
+
66
+ stdout_thread = threading.Thread(
67
+ target=stream_output,
68
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
69
+ )
70
+ stderr_thread = threading.Thread(
71
+ target=stream_output,
72
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
73
+ )
74
+ stdout_thread.start()
75
+ stderr_thread.start()
76
+ try:
77
+ return_code = process.wait(timeout=timeout)
78
+ except subprocess.TimeoutExpired:
79
+ process.kill()
80
+ self.report_error(
81
+ tr("\u274c Timed out after {timeout} seconds.", timeout=timeout)
82
+ )
83
+ return tr(
84
+ "Code timed out after {timeout} seconds.", timeout=timeout
85
+ )
86
+ stdout_thread.join()
87
+ stderr_thread.join()
88
+ stdout_file.flush()
89
+ stderr_file.flush()
90
+
91
+ self.report_success(
92
+ tr("\u2705 Return code {return_code}", return_code=return_code)
93
+ )
94
+ # Read back the content for summary if not too large
95
+ with open(
96
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
97
+ ) as out_f:
98
+ stdout_content = out_f.read()
99
+ with open(
100
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
101
+ ) as err_f:
102
+ stderr_content = err_f.read()
103
+ max_lines = 100
104
+ stdout_lines = stdout_content.count("\n")
105
+ stderr_lines = stderr_content.count("\n")
106
+
107
+ def head_tail(text, n=10):
108
+ lines = text.splitlines()
109
+ if len(lines) <= 2 * n:
110
+ return "\n".join(lines)
111
+ return "\n".join(
112
+ lines[:n]
113
+ + ["... ({} lines omitted) ...".format(len(lines) - 2 * n)]
114
+ + lines[-n:]
115
+ )
116
+
117
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
118
+ result = (
119
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
120
+ )
121
+ if stderr_content.strip():
122
+ result += f"\n--- STDERR ---\n{stderr_content}"
123
+ return result
124
+ else:
125
+ result = (
126
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
127
+ )
128
+ if stderr_lines > 0 and stderr_content.strip():
129
+ result += (
130
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
131
+ )
132
+ result += f"returncode: {return_code}\n"
133
+ result += (
134
+ "--- STDOUT (head/tail) ---\n"
135
+ + head_tail(stdout_content)
136
+ + "\n"
137
+ )
138
+ if stderr_content.strip():
139
+ result += (
140
+ "--- STDERR (head/tail) ---\n"
141
+ + head_tail(stderr_content)
142
+ + "\n"
143
+ )
144
+ result += "Use the get_lines tool to inspect the contents of these files when needed."
145
+ return result
146
+ except Exception as e:
147
+ self.report_error(tr("\u274c Error: {error}", error=e))
148
+ return tr("Error running file: {error}", error=e)
@@ -0,0 +1,154 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ import threading
5
+ from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
7
+ from janito.agent.tool_registry import register_tool
8
+ from janito.i18n import tr
9
+
10
+
11
+ @register_tool(name="python_stdin_runner")
12
+ class PythonStdinRunnerTool(ToolBase):
13
+ """
14
+ Tool to execute Python code by passing it to the interpreter via standard input (stdin).
15
+ Args:
16
+ code (str): The Python code to execute as a string.
17
+ timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
18
+ Returns:
19
+ str: Output and status message, or file paths/line counts if output is large.
20
+ """
21
+
22
+ def run(self, code: str, timeout: int = 60) -> str:
23
+ if not code.strip():
24
+ self.report_warning(tr("\u2139\ufe0f Empty code provided."))
25
+ return tr("Warning: Empty code provided. Operation skipped.")
26
+ self.report_info(
27
+ ActionType.EXECUTE,
28
+ tr("⚡ Running: python (stdin mode) ...\n{code}\n", code=code),
29
+ )
30
+ try:
31
+ with (
32
+ tempfile.NamedTemporaryFile(
33
+ mode="w+",
34
+ prefix="python_stdin_stdout_",
35
+ delete=False,
36
+ encoding="utf-8",
37
+ ) as stdout_file,
38
+ tempfile.NamedTemporaryFile(
39
+ mode="w+",
40
+ prefix="python_stdin_stderr_",
41
+ delete=False,
42
+ encoding="utf-8",
43
+ ) as stderr_file,
44
+ ):
45
+ process = subprocess.Popen(
46
+ [sys.executable],
47
+ stdin=subprocess.PIPE,
48
+ stdout=subprocess.PIPE,
49
+ stderr=subprocess.PIPE,
50
+ text=True,
51
+ bufsize=1,
52
+ universal_newlines=True,
53
+ encoding="utf-8",
54
+ env={**dict(), **dict(PYTHONIOENCODING="utf-8")},
55
+ )
56
+ stdout_lines = 0
57
+ stderr_lines = 0
58
+
59
+ def stream_output(stream, file_obj, report_func, count_func):
60
+ nonlocal stdout_lines, stderr_lines
61
+ for line in stream:
62
+ file_obj.write(line)
63
+ file_obj.flush()
64
+ report_func(line)
65
+ if count_func == "stdout":
66
+ stdout_lines += 1
67
+ else:
68
+ stderr_lines += 1
69
+
70
+ stdout_thread = threading.Thread(
71
+ target=stream_output,
72
+ args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
73
+ )
74
+ stderr_thread = threading.Thread(
75
+ target=stream_output,
76
+ args=(process.stderr, stderr_file, self.report_stderr, "stderr"),
77
+ )
78
+ stdout_thread.start()
79
+ stderr_thread.start()
80
+ try:
81
+ process.stdin.write(code)
82
+ process.stdin.close()
83
+ return_code = process.wait(timeout=timeout)
84
+ except subprocess.TimeoutExpired:
85
+ process.kill()
86
+ self.report_error(
87
+ tr("\u274c Timed out after {timeout} seconds.", timeout=timeout)
88
+ )
89
+ return tr(
90
+ "Code timed out after {timeout} seconds.", timeout=timeout
91
+ )
92
+ stdout_thread.join()
93
+ stderr_thread.join()
94
+ stdout_file.flush()
95
+ stderr_file.flush()
96
+
97
+ self.report_success(
98
+ tr("\u2705 Return code {return_code}", return_code=return_code)
99
+ )
100
+ # Read back the content for summary if not too large
101
+ with open(
102
+ stdout_file.name, "r", encoding="utf-8", errors="replace"
103
+ ) as out_f:
104
+ stdout_content = out_f.read()
105
+ with open(
106
+ stderr_file.name, "r", encoding="utf-8", errors="replace"
107
+ ) as err_f:
108
+ stderr_content = err_f.read()
109
+ max_lines = 100
110
+ stdout_lines = stdout_content.count("\n")
111
+ stderr_lines = stderr_content.count("\n")
112
+
113
+ def head_tail(text, n=10):
114
+ lines = text.splitlines()
115
+ if len(lines) <= 2 * n:
116
+ return "\n".join(lines)
117
+ return "\n".join(
118
+ lines[:n]
119
+ + ["... ({} lines omitted) ...".format(len(lines) - 2 * n)]
120
+ + lines[-n:]
121
+ )
122
+
123
+ if stdout_lines <= max_lines and stderr_lines <= max_lines:
124
+ result = (
125
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
126
+ )
127
+ if stderr_content.strip():
128
+ result += f"\n--- STDERR ---\n{stderr_content}"
129
+ return result
130
+ else:
131
+ result = (
132
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
133
+ )
134
+ if stderr_lines > 0 and stderr_content.strip():
135
+ result += (
136
+ f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
137
+ )
138
+ result += f"returncode: {return_code}\n"
139
+ result += (
140
+ "--- STDOUT (head/tail) ---\n"
141
+ + head_tail(stdout_content)
142
+ + "\n"
143
+ )
144
+ if stderr_content.strip():
145
+ result += (
146
+ "--- STDERR (head/tail) ---\n"
147
+ + head_tail(stderr_content)
148
+ + "\n"
149
+ )
150
+ result += "Use the get_lines tool to inspect the contents of these files when needed."
151
+ return result
152
+ except Exception as e:
153
+ self.report_error(tr("\u274c Error: {error}", error=e))
154
+ return tr("Error running code via stdin: {error}", error=e)
@@ -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.agent.tools_utils.utils import pluralize, display_path
4
5
  from janito.i18n import tr
@@ -24,7 +25,8 @@ class RemoveDirectoryTool(ToolBase):
24
25
  def run(self, file_path: str, recursive: bool = False) -> str:
25
26
  disp_path = display_path(file_path)
26
27
  self.report_info(
27
- tr("🗃️ Removing directory '{disp_path}' ...", disp_path=disp_path)
28
+ ActionType.WRITE,
29
+ tr("🗃️ Removing directory '{disp_path}' ...", disp_path=disp_path),
28
30
  )
29
31
  backup_zip = None
30
32
  try:
@@ -5,6 +5,7 @@ from janito.agent.tool_registry import register_tool
5
5
  # from janito.agent.tools_utils.expand_path import expand_path
6
6
  from janito.agent.tools_utils.utils import display_path
7
7
  from janito.agent.tool_base import ToolBase
8
+ from janito.agent.tools_utils.action_type import ActionType
8
9
  from janito.i18n import tr
9
10
 
10
11
 
@@ -28,7 +29,10 @@ class RemoveFileTool(ToolBase):
28
29
  disp_path = display_path(original_path)
29
30
  backup_path = None
30
31
  # Report initial info about what is going to be removed
31
- self.report_info(tr("🗑️ Removing file '{disp_path}' ...", disp_path=disp_path))
32
+ self.report_info(
33
+ ActionType.WRITE,
34
+ tr("🗑️ Removing file '{disp_path}' ...", disp_path=disp_path),
35
+ )
32
36
  if not os.path.exists(path):
33
37
  self.report_error(tr("❌ File does not exist."))
34
38
  return tr("❌ File does not exist.")
@@ -3,6 +3,7 @@ import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
4
  from janito.agent.tools_utils.utils import display_path
5
5
  from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
6
7
  from janito.i18n import tr
7
8
 
8
9
  from janito.agent.tools.validate_file_syntax.core import validate_file_syntax
@@ -18,6 +19,8 @@ class ReplaceFileTool(ToolBase):
18
19
  Returns:
19
20
  str: Status message indicating the result. Example:
20
21
  - "✅ Successfully replaced the file at ..."
22
+
23
+ Note: Syntax validation is automatically performed after this operation.
21
24
  """
22
25
 
23
26
  def run(self, file_path: str, content: str) -> str:
@@ -34,6 +37,10 @@ class ReplaceFileTool(ToolBase):
34
37
  # Check previous operation
35
38
  tracker = ToolUseTracker()
36
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
+ )
37
44
  self.report_warning(tr("ℹ️ Missing full view."))
38
45
  try:
39
46
  with open(file_path, "r", encoding="utf-8", errors="replace") as f:
@@ -41,11 +48,14 @@ class ReplaceFileTool(ToolBase):
41
48
  except Exception as e:
42
49
  current_content = f"[Error reading file: {e}]"
43
50
  return (
44
- "⚠️ [missing full view] You must fully view or replace the file before updating.\n"
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"
45
52
  f"--- Current content of {disp_path} ---\n"
46
53
  f"{current_content}"
47
54
  )
48
- self.report_info(tr("📝 Replacing file '{disp_path}' ...", disp_path=disp_path))
55
+ self.report_info(
56
+ ActionType.WRITE,
57
+ tr("📝 Replacing file '{disp_path}' ...", disp_path=disp_path),
58
+ )
49
59
  backup_path = file_path + ".bak"
50
60
  shutil.copy2(file_path, backup_path)
51
61
  with open(file_path, "w", encoding="utf-8", errors="replace") as f: