janito 2.22.0__py3-none-any.whl → 2.24.1__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 (67) hide show
  1. janito/README.md +0 -0
  2. janito/agent/setup_agent.py +14 -0
  3. janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +59 -11
  4. janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +53 -7
  5. janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +108 -8
  6. janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +53 -1
  7. janito/cli/chat_mode/session.py +8 -1
  8. janito/cli/chat_mode/shell/commands/__init__.py +2 -0
  9. janito/cli/chat_mode/shell/commands/security/__init__.py +1 -0
  10. janito/cli/chat_mode/shell/commands/security/allowed_sites.py +94 -0
  11. janito/cli/chat_mode/shell/commands/security_command.py +51 -0
  12. janito/cli/chat_mode/shell/commands.bak.zip +0 -0
  13. janito/cli/chat_mode/shell/session.bak.zip +0 -0
  14. janito/cli/cli_commands/list_plugins.py +45 -0
  15. janito/cli/cli_commands/show_system_prompt.py +13 -40
  16. janito/cli/core/getters.py +4 -0
  17. janito/cli/core/runner.py +7 -2
  18. janito/cli/core/setters.py +10 -1
  19. janito/cli/main_cli.py +25 -3
  20. janito/cli/single_shot_mode/handler.py +3 -1
  21. janito/config_manager.py +10 -0
  22. janito/docs/GETTING_STARTED.md +0 -0
  23. janito/drivers/dashscope.bak.zip +0 -0
  24. janito/drivers/openai/README.md +0 -0
  25. janito/drivers/openai_responses.bak.zip +0 -0
  26. janito/llm/README.md +0 -0
  27. janito/mkdocs.yml +0 -0
  28. janito/plugins/__init__.py +17 -0
  29. janito/plugins/base.py +93 -0
  30. janito/plugins/discovery.py +160 -0
  31. janito/plugins/manager.py +185 -0
  32. janito/providers/dashscope.bak.zip +0 -0
  33. janito/providers/ibm/README.md +0 -0
  34. janito/shell.bak.zip +0 -0
  35. janito/tools/DOCSTRING_STANDARD.txt +0 -0
  36. janito/tools/README.md +0 -0
  37. janito/tools/adapters/local/__init__.py +2 -0
  38. janito/tools/adapters/local/adapter.py +55 -0
  39. janito/tools/adapters/local/ask_user.py +2 -0
  40. janito/tools/adapters/local/fetch_url.py +89 -4
  41. janito/tools/adapters/local/find_files.py +2 -0
  42. janito/tools/adapters/local/get_file_outline/core.py +2 -0
  43. janito/tools/adapters/local/get_file_outline/search_outline.py +2 -0
  44. janito/tools/adapters/local/open_html_in_browser.py +2 -0
  45. janito/tools/adapters/local/open_url.py +2 -0
  46. janito/tools/adapters/local/python_code_run.py +15 -10
  47. janito/tools/adapters/local/python_command_run.py +14 -9
  48. janito/tools/adapters/local/python_file_run.py +15 -10
  49. janito/tools/adapters/local/read_chart.py +252 -0
  50. janito/tools/adapters/local/read_files.py +2 -0
  51. janito/tools/adapters/local/replace_text_in_file.py +1 -1
  52. janito/tools/adapters/local/run_bash_command.py +18 -12
  53. janito/tools/adapters/local/run_powershell_command.py +15 -9
  54. janito/tools/adapters/local/search_text/core.py +2 -0
  55. janito/tools/adapters/local/validate_file_syntax/core.py +6 -0
  56. janito/tools/adapters/local/validate_file_syntax/jinja2_validator.py +47 -0
  57. janito/tools/adapters/local/view_file.py +2 -0
  58. janito/tools/loop_protection.py +115 -0
  59. janito/tools/loop_protection_decorator.py +110 -0
  60. janito/tools/outline_file.bak.zip +0 -0
  61. janito/tools/url_whitelist.py +121 -0
  62. {janito-2.22.0.dist-info → janito-2.24.1.dist-info}/METADATA +411 -411
  63. {janito-2.22.0.dist-info → janito-2.24.1.dist-info}/RECORD +52 -39
  64. {janito-2.22.0.dist-info → janito-2.24.1.dist-info}/entry_points.txt +0 -0
  65. {janito-2.22.0.dist-info → janito-2.24.1.dist-info}/licenses/LICENSE +0 -0
  66. {janito-2.22.0.dist-info → janito-2.24.1.dist-info}/top_level.txt +0 -0
  67. {janito-2.22.0.dist-info → janito-2.24.1.dist-info}/WHEEL +0 -0
@@ -1,5 +1,6 @@
1
1
  from typing import Type, Dict, Any
2
2
  from janito.tools.tools_adapter import ToolsAdapterBase as ToolsAdapter
3
+ from janito.tools.tool_use_tracker import ToolUseTracker
3
4
 
4
5
 
5
6
  class LocalToolsAdapter(ToolsAdapter):
@@ -58,6 +59,9 @@ class LocalToolsAdapter(ToolsAdapter):
58
59
  # consistency with many file-system tools.
59
60
  os.chdir(self.workdir)
60
61
 
62
+ # Initialize tool tracker
63
+ self.tool_tracker = ToolUseTracker.instance()
64
+
61
65
  if tools:
62
66
  for tool in tools:
63
67
  self.register_tool(tool)
@@ -130,6 +134,57 @@ class LocalToolsAdapter(ToolsAdapter):
130
134
  and not is_tool_disabled(entry["instance"].tool_name)
131
135
  ]
132
136
 
137
+ # ------------------------------------------------------------------
138
+ # Tool execution with error handling
139
+ # ------------------------------------------------------------------
140
+ def execute_tool(self, name: str, **kwargs):
141
+ """
142
+ Execute a tool with proper error handling.
143
+
144
+ This method extends the base execute_tool functionality by adding
145
+ error handling for RuntimeError exceptions that may be raised by
146
+ tools with loop protection decorators.
147
+
148
+ Args:
149
+ name: The name of the tool to execute
150
+ **kwargs: Arguments to pass to the tool
151
+
152
+ Returns:
153
+ The result of the tool execution
154
+
155
+ Raises:
156
+ ToolCallException: If tool execution fails for any reason
157
+ ValueError: If the tool is not found or not allowed
158
+ """
159
+ # First check if tool exists and is allowed
160
+ tool = self.get_tool(name)
161
+ if not tool:
162
+ raise ValueError(f"Tool '{name}' not found or not allowed.")
163
+
164
+ # Record tool usage
165
+ self.tool_tracker.record(name, kwargs)
166
+
167
+ # Execute the tool and handle any RuntimeError from loop protection
168
+ try:
169
+ return super().execute_tool(name, **kwargs)
170
+ except RuntimeError as e:
171
+ # Check if this is a loop protection error
172
+ if "Loop protection:" in str(e):
173
+ # Re-raise as ToolCallException to maintain consistent error flow
174
+ from janito.exceptions import ToolCallException
175
+ raise ToolCallException(
176
+ name,
177
+ f"Loop protection triggered: {str(e)}",
178
+ arguments=kwargs
179
+ )
180
+ # Re-raise other RuntimeError exceptions as ToolCallException
181
+ from janito.exceptions import ToolCallException
182
+ raise ToolCallException(
183
+ name,
184
+ f"Runtime error during tool execution: {str(e)}",
185
+ arguments=kwargs
186
+ )
187
+
133
188
  # ------------------------------------------------------------------
134
189
  # Convenience methods
135
190
  # ------------------------------------------------------------------
@@ -1,5 +1,6 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.tools.adapters.local.adapter import register_local_tool
3
+ from janito.tools.loop_protection_decorator import protect_against_loops
3
4
 
4
5
  from rich import print as rich_print
5
6
  from janito.i18n import tr
@@ -31,6 +32,7 @@ class AskUserTool(ToolBase):
31
32
  permissions = ToolPermissions(read=True)
32
33
  tool_name = "ask_user"
33
34
 
35
+ @protect_against_loops(max_calls=5, time_window=10.0)
34
36
  def run(self, question: str) -> str:
35
37
 
36
38
  print() # Print an empty line before the question panel
@@ -9,6 +9,7 @@ from janito.tools.tool_base import ToolBase, ToolPermissions
9
9
  from janito.report_events import ReportAction
10
10
  from janito.i18n import tr
11
11
  from janito.tools.tool_utils import pluralize
12
+ from janito.tools.loop_protection_decorator import protect_against_loops
12
13
 
13
14
 
14
15
  @register_local_tool
@@ -16,6 +17,25 @@ class FetchUrlTool(ToolBase):
16
17
  """
17
18
  Fetch the content of a web page and extract its text.
18
19
 
20
+ This tool implements a **session-based caching mechanism** that provides
21
+ **in-memory caching** for the lifetime of the tool instance. URLs are cached
22
+ in RAM during the session, providing instant access to previously fetched
23
+ content without making additional HTTP requests.
24
+
25
+ **Session Cache Behavior:**
26
+ - **Lifetime**: Cache exists for the lifetime of the FetchUrlTool instance
27
+ - **Scope**: In-memory (RAM) cache, not persisted to disk
28
+ - **Storage**: Successful responses are cached as raw HTML content
29
+ - **Key**: Cache key is the exact URL string
30
+ - **Invalidation**: Cache is automatically cleared when the tool instance is destroyed
31
+ - **Performance**: Subsequent requests for the same URL return instantly
32
+
33
+ **Error Cache Behavior:**
34
+ - HTTP 403 errors: Cached for 24 hours (more permanent)
35
+ - HTTP 404 errors: Cached for 1 hour (temporary)
36
+ - Other 4xx errors: Cached for 30 minutes
37
+ - 5xx errors: Not cached (retried on each request)
38
+
19
39
  Args:
20
40
  url (str): The URL of the web page to fetch.
21
41
  search_strings (list[str], optional): Strings to search for in the page content.
@@ -23,6 +43,8 @@ class FetchUrlTool(ToolBase):
23
43
  max_lines (int, optional): Maximum number of lines to return. Defaults to 200.
24
44
  context_chars (int, optional): Characters of context around search matches. Defaults to 400.
25
45
  timeout (int, optional): Timeout in seconds for the HTTP request. Defaults to 10.
46
+ save_to_file (str, optional): File path to save the full resource content. If provided,
47
+ the complete response will be saved to this file instead of being processed.
26
48
  Returns:
27
49
  str: Extracted text content from the web page, or a warning message. Example:
28
50
  - "<main text content...>"
@@ -38,6 +60,7 @@ class FetchUrlTool(ToolBase):
38
60
  self.cache_dir = Path.home() / ".janito" / "cache" / "fetch_url"
39
61
  self.cache_dir.mkdir(parents=True, exist_ok=True)
40
62
  self.cache_file = self.cache_dir / "error_cache.json"
63
+ self.session_cache = {} # In-memory session cache - lifetime matches tool instance
41
64
  self._load_cache()
42
65
 
43
66
  def _load_cache(self):
@@ -99,8 +122,41 @@ class FetchUrlTool(ToolBase):
99
122
  self._save_cache()
100
123
 
101
124
  def _fetch_url_content(self, url: str, timeout: int = 10) -> str:
102
- """Fetch URL content and handle HTTP errors."""
103
- # Check cache first for known errors
125
+ """Fetch URL content and handle HTTP errors.
126
+
127
+ Implements two-tier caching:
128
+ 1. Session cache: In-memory cache for successful responses (lifetime = tool instance)
129
+ 2. Error cache: Persistent disk cache for HTTP errors with different expiration times
130
+
131
+ Also implements URL whitelist checking.
132
+ """
133
+ # Check URL whitelist
134
+ from janito.tools.url_whitelist import get_url_whitelist_manager
135
+ whitelist_manager = get_url_whitelist_manager()
136
+
137
+ if not whitelist_manager.is_url_allowed(url):
138
+ error_message = tr(
139
+ "Warning: URL blocked by whitelist: {url}",
140
+ url=url,
141
+ )
142
+ self.report_error(
143
+ tr(
144
+ "❗ URL blocked by whitelist: {url}",
145
+ url=url,
146
+ ),
147
+ ReportAction.READ,
148
+ )
149
+ return error_message
150
+
151
+ # Check session cache first
152
+ if url in self.session_cache:
153
+ self.report_warning(
154
+ tr("ℹ️ Using session cache"),
155
+ ReportAction.READ,
156
+ )
157
+ return self.session_cache[url]
158
+
159
+ # Check persistent cache for known errors
104
160
  cached_error, is_cached = self._get_cached_error(url)
105
161
  if cached_error:
106
162
  self.report_warning(
@@ -115,7 +171,10 @@ class FetchUrlTool(ToolBase):
115
171
  try:
116
172
  response = requests.get(url, timeout=timeout)
117
173
  response.raise_for_status()
118
- return response.text
174
+ content = response.text
175
+ # Cache successful responses in session cache
176
+ self.session_cache[url] = content
177
+ return content
119
178
  except requests.exceptions.HTTPError as http_err:
120
179
  status_code = http_err.response.status_code if http_err.response else None
121
180
  if status_code and 400 <= status_code < 500:
@@ -203,6 +262,7 @@ class FetchUrlTool(ToolBase):
203
262
 
204
263
  return text
205
264
 
265
+ @protect_against_loops(max_calls=5, time_window=10.0)
206
266
  def run(
207
267
  self,
208
268
  url: str,
@@ -211,6 +271,7 @@ class FetchUrlTool(ToolBase):
211
271
  max_lines: int = 200,
212
272
  context_chars: int = 400,
213
273
  timeout: int = 10,
274
+ save_to_file: str = None,
214
275
  ) -> str:
215
276
  if not url.strip():
216
277
  self.report_warning(tr("ℹ️ Empty URL provided."), ReportAction.READ)
@@ -218,7 +279,31 @@ class FetchUrlTool(ToolBase):
218
279
 
219
280
  self.report_action(tr("🌐 Fetch URL '{url}' ...", url=url), ReportAction.READ)
220
281
 
221
- # Fetch URL content
282
+ # Check if we should save to file
283
+ if save_to_file:
284
+ html_content = self._fetch_url_content(url, timeout=timeout)
285
+ if html_content.startswith("Warning:"):
286
+ return html_content
287
+
288
+ try:
289
+ with open(save_to_file, 'w', encoding='utf-8') as f:
290
+ f.write(html_content)
291
+ file_size = len(html_content)
292
+ self.report_success(
293
+ tr(
294
+ "✅ Saved {size} bytes to {file}",
295
+ size=file_size,
296
+ file=save_to_file,
297
+ ),
298
+ ReportAction.READ,
299
+ )
300
+ return tr("Successfully saved content to: {file}", file=save_to_file)
301
+ except IOError as e:
302
+ error_msg = tr("Error saving to file: {error}", error=str(e))
303
+ self.report_error(error_msg, ReportAction.READ)
304
+ return error_msg
305
+
306
+ # Normal processing path
222
307
  html_content = self._fetch_url_content(url, timeout=timeout)
223
308
  if html_content.startswith("Warning:"):
224
309
  return html_content
@@ -6,6 +6,7 @@ from janito.dir_walk_utils import walk_dir_with_gitignore
6
6
  from janito.i18n import tr
7
7
  import fnmatch
8
8
  import os
9
+ from janito.tools.loop_protection_decorator import protect_against_loops
9
10
 
10
11
 
11
12
  @register_local_tool
@@ -107,6 +108,7 @@ class FindFilesTool(ToolBase):
107
108
  }
108
109
  return sorted(dir_output)
109
110
 
111
+ @protect_against_loops(max_calls=5, time_window=10.0)
110
112
  def run(
111
113
  self,
112
114
  paths: str,
@@ -10,6 +10,7 @@ from janito.tools.tool_utils import display_path, pluralize
10
10
  from janito.i18n import tr
11
11
 
12
12
  from janito.tools.adapters.local.adapter import register_local_tool as register_tool
13
+ from janito.tools.loop_protection_decorator import protect_against_loops
13
14
 
14
15
 
15
16
  @register_tool
@@ -24,6 +25,7 @@ class GetFileOutlineTool(ToolBase):
24
25
  permissions = ToolPermissions(read=True)
25
26
  tool_name = "get_file_outline"
26
27
 
28
+ @protect_against_loops(max_calls=5, time_window=10.0)
27
29
  def run(self, path: str) -> str:
28
30
  try:
29
31
  self.report_action(
@@ -1,5 +1,6 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.report_events import ReportAction
3
+ from janito.tools.loop_protection_decorator import protect_against_loops
3
4
 
4
5
 
5
6
  class SearchOutlineTool(ToolBase):
@@ -15,6 +16,7 @@ class SearchOutlineTool(ToolBase):
15
16
  permissions = ToolPermissions(read=True)
16
17
  tool_name = "search_outline"
17
18
 
19
+ @protect_against_loops(max_calls=5, time_window=10.0)
18
20
  def run(self, path: str) -> str:
19
21
  from janito.tools.tool_utils import display_path
20
22
  from janito.i18n import tr
@@ -4,6 +4,7 @@ from janito.tools.adapters.local.adapter import register_local_tool
4
4
  from janito.tools.tool_base import ToolBase, ToolPermissions
5
5
  from janito.report_events import ReportAction
6
6
  from janito.i18n import tr
7
+ from janito.tools.loop_protection_decorator import protect_against_loops
7
8
 
8
9
 
9
10
  @register_local_tool
@@ -20,6 +21,7 @@ class OpenHtmlInBrowserTool(ToolBase):
20
21
  permissions = ToolPermissions(read=True)
21
22
  tool_name = "open_html_in_browser"
22
23
 
24
+ @protect_against_loops(max_calls=5, time_window=10.0)
23
25
  def run(self, path: str) -> str:
24
26
  if not path.strip():
25
27
  self.report_warning(tr("ℹ️ Empty file path provided."))
@@ -3,6 +3,7 @@ from janito.tools.adapters.local.adapter import register_local_tool
3
3
  from janito.tools.tool_base import ToolBase, ToolPermissions
4
4
  from janito.report_events import ReportAction
5
5
  from janito.i18n import tr
6
+ from janito.tools.loop_protection_decorator import protect_against_loops
6
7
 
7
8
 
8
9
  @register_local_tool
@@ -19,6 +20,7 @@ class OpenUrlTool(ToolBase):
19
20
  permissions = ToolPermissions(read=True)
20
21
  tool_name = "open_url"
21
22
 
23
+ @protect_against_loops(max_calls=5, time_window=10.0)
22
24
  def run(self, url: str) -> str:
23
25
  if not url.strip():
24
26
  self.report_warning(tr("ℹ️ Empty URL provided."))
@@ -17,6 +17,7 @@ class PythonCodeRunTool(ToolBase):
17
17
  Args:
18
18
  code (str): The Python code to execute as a string.
19
19
  timeout (int): Timeout in seconds for the command. Defaults to 60.
20
+ silent (bool): If True, suppresses progress and status messages. Defaults to False.
20
21
 
21
22
  Returns:
22
23
  str: Output and status message, or file paths/line counts if output is large.
@@ -25,15 +26,18 @@ class PythonCodeRunTool(ToolBase):
25
26
  permissions = ToolPermissions(execute=True)
26
27
  tool_name = "python_code_run"
27
28
 
28
- def run(self, code: str, timeout: int = 60) -> str:
29
+ def run(self, code: str, timeout: int = 60, silent: bool = False) -> str:
29
30
  if not code.strip():
30
31
  self.report_warning(tr("ℹ️ Empty code provided."), ReportAction.EXECUTE)
31
32
  return tr("Warning: Empty code provided. Operation skipped.")
32
- self.report_action(
33
- tr("⚡ Running: python (stdin mode) ...\n{code}\n", code=code),
34
- ReportAction.EXECUTE,
35
- )
36
- self.report_stdout("\n")
33
+ if not silent:
34
+ self.report_action(
35
+ tr("⚡ Running: python (stdin mode) ...\n{code}\n", code=code),
36
+ ReportAction.EXECUTE,
37
+ )
38
+ self.report_stdout("\n")
39
+ else:
40
+ self.report_action(tr("⚡ Executing..."), ReportAction.EXECUTE)
37
41
  try:
38
42
  with (
39
43
  tempfile.NamedTemporaryFile(
@@ -70,10 +74,11 @@ class PythonCodeRunTool(ToolBase):
70
74
  )
71
75
  stdout_file.flush()
72
76
  stderr_file.flush()
73
- self.report_success(
74
- tr("✅ Return code {return_code}", return_code=return_code),
75
- ReportAction.EXECUTE,
76
- )
77
+ if not silent:
78
+ self.report_success(
79
+ tr("✅ Return code {return_code}", return_code=return_code),
80
+ ReportAction.EXECUTE,
81
+ )
77
82
  return self._format_result(
78
83
  stdout_file.name, stderr_file.name, return_code
79
84
  )
@@ -17,6 +17,7 @@ class PythonCommandRunTool(ToolBase):
17
17
  Args:
18
18
  code (str): The Python code to execute as a string.
19
19
  timeout (int): Timeout in seconds for the command. Defaults to 60.
20
+ silent (bool): If True, suppresses progress and status messages. Defaults to False.
20
21
 
21
22
  Returns:
22
23
  str: Output and status message, or file paths/line counts if output is large.
@@ -25,14 +26,17 @@ class PythonCommandRunTool(ToolBase):
25
26
  permissions = ToolPermissions(execute=True)
26
27
  tool_name = "python_command_run"
27
28
 
28
- def run(self, code: str, timeout: int = 60) -> str:
29
+ def run(self, code: str, timeout: int = 60, silent: bool = False) -> str:
29
30
  if not code.strip():
30
31
  self.report_warning(tr("ℹ️ Empty code provided."), ReportAction.EXECUTE)
31
32
  return tr("Warning: Empty code provided. Operation skipped.")
32
- self.report_action(
33
- tr("🐍 Running: python -c ...\n{code}\n", code=code), ReportAction.EXECUTE
34
- )
35
- self.report_stdout("\n")
33
+ if not silent:
34
+ self.report_action(
35
+ tr("🐍 Running: python -c ...\n{code}\n", code=code), ReportAction.EXECUTE
36
+ )
37
+ self.report_stdout("\n")
38
+ else:
39
+ self.report_action(tr("⚡ Executing..."), ReportAction.EXECUTE)
36
40
  try:
37
41
  with (
38
42
  tempfile.NamedTemporaryFile(
@@ -68,10 +72,11 @@ class PythonCommandRunTool(ToolBase):
68
72
  )
69
73
  stdout_file.flush()
70
74
  stderr_file.flush()
71
- self.report_success(
72
- tr("✅ Return code {return_code}", return_code=return_code),
73
- ReportAction.EXECUTE,
74
- )
75
+ if not silent:
76
+ self.report_success(
77
+ tr("✅ Return code {return_code}", return_code=return_code),
78
+ ReportAction.EXECUTE,
79
+ )
75
80
  return self._format_result(
76
81
  stdout_file.name, stderr_file.name, return_code
77
82
  )
@@ -17,6 +17,7 @@ class PythonFileRunTool(ToolBase):
17
17
  Args:
18
18
  path (str): Path to the Python script file to execute.
19
19
  timeout (int): Timeout in seconds for the command. Defaults to 60.
20
+ silent (bool): If True, suppresses progress and status messages. Defaults to False.
20
21
 
21
22
  Returns:
22
23
  str: Output and status message, or file paths/line counts if output is large.
@@ -25,12 +26,15 @@ class PythonFileRunTool(ToolBase):
25
26
  permissions = ToolPermissions(execute=True)
26
27
  tool_name = "python_file_run"
27
28
 
28
- def run(self, path: str, timeout: int = 60) -> str:
29
- self.report_action(
30
- tr("🚀 Running: python {path}", path=path),
31
- ReportAction.EXECUTE,
32
- )
33
- self.report_stdout("\n")
29
+ def run(self, path: str, timeout: int = 60, silent: bool = False) -> str:
30
+ if not silent:
31
+ self.report_action(
32
+ tr("🚀 Running: python {path}", path=path),
33
+ ReportAction.EXECUTE,
34
+ )
35
+ self.report_stdout("\n")
36
+ else:
37
+ self.report_action(tr("⚡ Executing..."), ReportAction.EXECUTE)
34
38
  try:
35
39
  with (
36
40
  tempfile.NamedTemporaryFile(
@@ -66,10 +70,11 @@ class PythonFileRunTool(ToolBase):
66
70
  )
67
71
  stdout_file.flush()
68
72
  stderr_file.flush()
69
- self.report_success(
70
- tr("✅ Return code {return_code}", return_code=return_code),
71
- ReportAction.EXECUTE,
72
- )
73
+ if not silent:
74
+ self.report_success(
75
+ tr("✅ Return code {return_code}", return_code=return_code),
76
+ ReportAction.EXECUTE,
77
+ )
73
78
  return self._format_result(
74
79
  stdout_file.name, stderr_file.name, return_code
75
80
  )