janito 3.5.1__py3-none-any.whl → 3.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 (161) hide show
  1. janito/README.md +1 -4
  2. janito/cli/chat_mode/bindings.py +0 -50
  3. janito/cli/chat_mode/session.py +1 -12
  4. janito/cli/chat_mode/shell/commands/multi.py +0 -5
  5. janito/cli/chat_mode/shell/commands/security/allowed_sites.py +33 -47
  6. janito/cli/cli_commands/list_plugins.py +43 -52
  7. janito/cli/core/getters.py +0 -3
  8. janito/cli/core/model_guesser.py +24 -40
  9. janito/cli/main_cli.py +12 -9
  10. janito/cli/prompt_core.py +9 -47
  11. janito/cli/rich_terminal_reporter.py +2 -2
  12. janito/docs/GETTING_STARTED.md +1 -1
  13. janito/drivers/openai/driver.py +0 -1
  14. janito/drivers/zai/driver.py +0 -1
  15. janito/i18n/it.py +46 -46
  16. janito/llm/agent.py +16 -32
  17. janito/llm/auth_utils.py +5 -14
  18. janito/llm/driver.py +0 -8
  19. janito/plugins/__init__.py +12 -31
  20. janito/plugins/auto_loader.py +11 -12
  21. janito/plugins/auto_loader_fixed.py +11 -12
  22. janito/{plugin_system → plugins}/base.py +2 -5
  23. janito/plugins/builtin.py +1 -15
  24. janito/plugins/core_adapter.py +11 -89
  25. janito/plugins/core_loader.py +120 -0
  26. janito/plugins/core_loader_fixed.py +125 -0
  27. janito/plugins/discovery.py +5 -5
  28. janito/plugins/discovery_core.py +9 -14
  29. janito/plugins/manager.py +1 -1
  30. janito/providers/__init__.py +0 -1
  31. janito/providers/moonshot/model_info.py +11 -0
  32. janito/tools/__init__.py +7 -31
  33. janito/tools/adapters/__init__.py +1 -6
  34. janito/tools/adapters/local/__init__.py +70 -7
  35. janito/{plugins/tools → tools/adapters/local}/ask_user.py +3 -3
  36. janito/{plugins/tools → tools/adapters/local}/create_file.py +6 -6
  37. janito/{plugins/tools → tools/adapters/local}/fetch_url.py +3 -3
  38. janito/{plugins/tools → tools/adapters/local}/python_code_run.py +7 -23
  39. janito/{plugins/tools → tools/adapters/local}/python_command_run.py +5 -21
  40. janito/{plugins/tools → tools/adapters/local}/python_file_run.py +5 -21
  41. janito/{plugins/tools → tools/adapters/local}/replace_text_in_file.py +4 -4
  42. janito/{plugins/tools → tools/adapters/local}/run_bash_command.py +3 -3
  43. janito/{plugins/tools → tools/adapters/local}/run_powershell_command.py +3 -3
  44. janito/{plugins/tools → tools/adapters/local}/show_image.py +6 -15
  45. janito/{plugins/core/imagedisplay/tools → tools/adapters/local}/show_image_grid.py +5 -13
  46. janito/tools/function_adapter.py +65 -0
  47. janito/tools/loop_protection_decorator.py +117 -114
  48. janito-3.7.0.dist-info/METADATA +84 -0
  49. {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/RECORD +88 -157
  50. janito/cli/cli_commands/check_tools.py +0 -212
  51. janito/llm/cancellation_manager.py +0 -63
  52. janito/llm/enter_cancellation.py +0 -107
  53. janito/plugin_system/__init__.py +0 -10
  54. janito/plugin_system/core_loader.py +0 -217
  55. janito/plugin_system/core_loader_fixed.py +0 -225
  56. janito/plugins/core/__init__.py +0 -7
  57. janito/plugins/core/codeanalyzer/__init__.py +0 -43
  58. janito/plugins/core/filemanager/__init__.py +0 -124
  59. janito/plugins/core/filemanager/tools/create_file.py +0 -87
  60. janito/plugins/core/filemanager/tools/replace_text_in_file.py +0 -270
  61. janito/plugins/core/imagedisplay/__init__.py +0 -14
  62. janito/plugins/core/imagedisplay/plugin.py +0 -51
  63. janito/plugins/core/imagedisplay/tools/__init__.py +0 -1
  64. janito/plugins/core/imagedisplay/tools/show_image.py +0 -83
  65. janito/plugins/core/system/__init__.py +0 -23
  66. janito/plugins/core/system/tools/run_bash_command.py +0 -204
  67. janito/plugins/core/system/tools/run_powershell_command.py +0 -234
  68. janito/plugins/dev/__init__.py +0 -7
  69. janito/plugins/dev/pythondev/__init__.py +0 -37
  70. janito/plugins/dev/visualization/__init__.py +0 -23
  71. janito/plugins/example_plugin.py +0 -108
  72. janito/plugins/tools/__init__.py +0 -10
  73. janito/plugins/tools/copy_file.py +0 -87
  74. janito/plugins/tools/core_tools_plugin.py +0 -87
  75. janito/plugins/tools/create_directory.py +0 -70
  76. janito/plugins/tools/decorators.py +0 -19
  77. janito/plugins/tools/delete_text_in_file.py +0 -134
  78. janito/plugins/tools/find_files.py +0 -143
  79. janito/plugins/tools/get_file_outline/__init__.py +0 -7
  80. janito/plugins/tools/get_file_outline/core.py +0 -122
  81. janito/plugins/tools/get_file_outline/java_outline.py +0 -47
  82. janito/plugins/tools/get_file_outline/markdown_outline.py +0 -14
  83. janito/plugins/tools/get_file_outline/python_outline.py +0 -303
  84. janito/plugins/tools/get_file_outline/search_outline.py +0 -36
  85. janito/plugins/tools/move_file.py +0 -131
  86. janito/plugins/tools/open_html_in_browser.py +0 -51
  87. janito/plugins/tools/open_url.py +0 -37
  88. janito/plugins/tools/read_chart.py +0 -259
  89. janito/plugins/tools/read_files.py +0 -58
  90. janito/plugins/tools/remove_directory.py +0 -55
  91. janito/plugins/tools/remove_file.py +0 -58
  92. janito/plugins/tools/search_text/__init__.py +0 -7
  93. janito/plugins/tools/search_text/core.py +0 -205
  94. janito/plugins/tools/search_text/match_lines.py +0 -67
  95. janito/plugins/tools/search_text/pattern_utils.py +0 -73
  96. janito/plugins/tools/search_text/traverse_directory.py +0 -145
  97. janito/plugins/tools/show_image_grid.py +0 -85
  98. janito/plugins/tools/validate_file_syntax/__init__.py +0 -7
  99. janito/plugins/tools/validate_file_syntax/core.py +0 -114
  100. janito/plugins/tools/validate_file_syntax/css_validator.py +0 -35
  101. janito/plugins/tools/validate_file_syntax/html_validator.py +0 -100
  102. janito/plugins/tools/validate_file_syntax/jinja2_validator.py +0 -50
  103. janito/plugins/tools/validate_file_syntax/js_validator.py +0 -27
  104. janito/plugins/tools/validate_file_syntax/json_validator.py +0 -6
  105. janito/plugins/tools/validate_file_syntax/markdown_validator.py +0 -109
  106. janito/plugins/tools/validate_file_syntax/ps1_validator.py +0 -32
  107. janito/plugins/tools/validate_file_syntax/python_validator.py +0 -5
  108. janito/plugins/tools/validate_file_syntax/xml_validator.py +0 -11
  109. janito/plugins/tools/validate_file_syntax/yaml_validator.py +0 -6
  110. janito/plugins/tools/view_file.py +0 -172
  111. janito/plugins/ui/__init__.py +0 -7
  112. janito/plugins/ui/userinterface/__init__.py +0 -16
  113. janito/plugins/ui/userinterface/tools/ask_user.py +0 -110
  114. janito/plugins/web/__init__.py +0 -7
  115. janito/plugins/web/webtools/__init__.py +0 -33
  116. janito/plugins/web/webtools/tools/fetch_url.py +0 -458
  117. janito/providers/together/__init__.py +0 -1
  118. janito/providers/together/model_info.py +0 -69
  119. janito/providers/together/provider.py +0 -108
  120. janito/tools/cli_initializer.py +0 -88
  121. janito/tools/initialize.py +0 -70
  122. janito-3.5.1.dist-info/METADATA +0 -229
  123. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/copy_file.py +0 -0
  124. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/create_directory.py +0 -0
  125. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/delete_text_in_file.py +0 -0
  126. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/find_files.py +0 -0
  127. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
  128. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/core.py +0 -0
  129. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/java_outline.py +0 -0
  130. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
  131. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/python_outline.py +0 -0
  132. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/search_outline.py +0 -0
  133. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/move_file.py +0 -0
  134. /janito/{plugins/web/webtools/tools → tools/adapters/local}/open_html_in_browser.py +0 -0
  135. /janito/{plugins/web/webtools/tools → tools/adapters/local}/open_url.py +0 -0
  136. /janito/{plugins/dev/visualization/tools → tools/adapters/local}/read_chart.py +0 -0
  137. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/read_files.py +0 -0
  138. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/remove_directory.py +0 -0
  139. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/remove_file.py +0 -0
  140. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/__init__.py +0 -0
  141. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/core.py +0 -0
  142. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/match_lines.py +0 -0
  143. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/pattern_utils.py +0 -0
  144. /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/traverse_directory.py +0 -0
  145. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
  146. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/core.py +0 -0
  147. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +0 -0
  148. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +0 -0
  149. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/jinja2_validator.py +0 -0
  150. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +0 -0
  151. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +0 -0
  152. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +0 -0
  153. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +0 -0
  154. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +0 -0
  155. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +0 -0
  156. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +0 -0
  157. /janito/{plugins/core/filemanager/tools → tools/adapters/local}/view_file.py +0 -0
  158. {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/WHEEL +0 -0
  159. {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/entry_points.txt +0 -0
  160. {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/licenses/LICENSE +0 -0
  161. {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/top_level.txt +0 -0
@@ -5,12 +5,12 @@ import tempfile
5
5
  import threading
6
6
  from janito.tools.tool_base import ToolBase, ToolPermissions
7
7
  from janito.report_events import ReportAction
8
- from janito.plugins.tools.decorators import register_core_tool
8
+ from janito.tools.adapters.local.adapter import register_local_tool
9
9
  from janito.i18n import tr
10
10
 
11
11
 
12
- @register_core_tool
13
- class PythonCommandRun(ToolBase):
12
+ @register_local_tool
13
+ class PythonCommandRunTool(ToolBase):
14
14
  """
15
15
  Tool to execute Python code using the `python -c` command-line flag.
16
16
 
@@ -92,9 +92,6 @@ class PythonCommandRun(ToolBase):
92
92
  def stream_output(stream, file_obj, report_func, count_func):
93
93
  nonlocal stdout_lines, stderr_lines
94
94
  for line in stream:
95
- # Check for cancellation
96
- if hasattr(self, '_cancel_event') and self._cancel_event.is_set():
97
- break
98
95
  file_obj.write(line)
99
96
  file_obj.flush()
100
97
  from janito.tools.tool_base import ReportAction
@@ -105,11 +102,6 @@ class PythonCommandRun(ToolBase):
105
102
  else:
106
103
  stderr_lines += 1
107
104
 
108
- # Set up cancellation event
109
- from janito.llm.cancellation_manager import get_cancellation_manager
110
- cancel_manager = get_cancellation_manager()
111
- self._cancel_event = cancel_manager.get_current_cancel_event()
112
-
113
105
  stdout_thread = threading.Thread(
114
106
  target=stream_output,
115
107
  args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
@@ -120,16 +112,8 @@ class PythonCommandRun(ToolBase):
120
112
  )
121
113
  stdout_thread.start()
122
114
  stderr_thread.start()
123
-
124
- try:
125
- stdout_thread.join()
126
- stderr_thread.join()
127
- except Exception as e:
128
- # Handle cancellation
129
- if self._cancel_event and self._cancel_event.is_set():
130
- process.kill()
131
- self.report_warning(tr("Code execution cancelled by user"), ReportAction.EXECUTE)
132
- raise
115
+ stdout_thread.join()
116
+ stderr_thread.join()
133
117
  return stdout_lines, stderr_lines
134
118
 
135
119
  def _wait_for_process(self, process, timeout):
@@ -5,12 +5,12 @@ import tempfile
5
5
  import threading
6
6
  from janito.tools.tool_base import ToolBase, ToolPermissions
7
7
  from janito.report_events import ReportAction
8
- from janito.plugins.tools.decorators import register_core_tool
8
+ from janito.tools.adapters.local.adapter import register_local_tool
9
9
  from janito.i18n import tr
10
10
 
11
11
 
12
- @register_core_tool
13
- class PythonFileRun(ToolBase):
12
+ @register_local_tool
13
+ class PythonFileRunTool(ToolBase):
14
14
  """
15
15
  Tool to execute a specified Python script file.
16
16
 
@@ -92,9 +92,6 @@ class PythonFileRun(ToolBase):
92
92
  def stream_output(stream, file_obj, report_func, count_func):
93
93
  nonlocal stdout_lines, stderr_lines
94
94
  for line in stream:
95
- # Check for cancellation
96
- if hasattr(self, '_cancel_event') and self._cancel_event.is_set():
97
- break
98
95
  file_obj.write(line)
99
96
  file_obj.flush()
100
97
  # Always supply a default action for stdout/stderr reporting
@@ -106,11 +103,6 @@ class PythonFileRun(ToolBase):
106
103
  else:
107
104
  stderr_lines += 1
108
105
 
109
- # Set up cancellation event
110
- from janito.llm.cancellation_manager import get_cancellation_manager
111
- cancel_manager = get_cancellation_manager()
112
- self._cancel_event = cancel_manager.get_current_cancel_event()
113
-
114
106
  stdout_thread = threading.Thread(
115
107
  target=stream_output,
116
108
  args=(process.stdout, stdout_file, self.report_stdout, "stdout"),
@@ -121,16 +113,8 @@ class PythonFileRun(ToolBase):
121
113
  )
122
114
  stdout_thread.start()
123
115
  stderr_thread.start()
124
-
125
- try:
126
- stdout_thread.join()
127
- stderr_thread.join()
128
- except Exception as e:
129
- # Handle cancellation
130
- if self._cancel_event and self._cancel_event.is_set():
131
- process.kill()
132
- self.report_warning(tr("File execution cancelled by user"), ReportAction.EXECUTE)
133
- raise
116
+ stdout_thread.join()
117
+ stderr_thread.join()
134
118
  return stdout_lines, stderr_lines
135
119
 
136
120
  def _wait_for_process(self, process, timeout):
@@ -1,14 +1,14 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.report_events import ReportAction
3
- from janito.plugins.tools.decorators import register_core_tool
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
4
  from janito.i18n import tr
5
5
  import shutil
6
6
  import re
7
- from janito.plugins.tools.validate_file_syntax.core import validate_file_syntax
7
+ from janito.tools.adapters.local.validate_file_syntax.core import validate_file_syntax
8
8
 
9
9
 
10
- @register_core_tool
11
- class ReplaceTextInFile(ToolBase):
10
+ @register_local_tool
11
+ class ReplaceTextInFileTool(ToolBase):
12
12
  """
13
13
  Replace exact occurrences of a given text in a file.
14
14
 
@@ -1,6 +1,6 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.report_events import ReportAction
3
- from janito.plugins.tools.decorators import register_core_tool
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
4
  from janito.i18n import tr
5
5
  import subprocess
6
6
  import tempfile
@@ -9,8 +9,8 @@ import os
9
9
  import threading
10
10
 
11
11
 
12
- @register_core_tool
13
- class RunBashCommand(ToolBase):
12
+ @register_local_tool
13
+ class RunBashCommandTool(ToolBase):
14
14
  """
15
15
  Execute a non-interactive command using the bash shell and capture live output.
16
16
  This tool explicitly invokes the 'bash' shell (not just the system default shell), so it requires bash to be installed and available in the system PATH. On Windows, this will only work if bash is available (e.g., via WSL, Git Bash, or similar).
@@ -1,6 +1,6 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.report_events import ReportAction
3
- from janito.plugins.tools.decorators import register_core_tool
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
4
  from janito.i18n import tr
5
5
  import subprocess
6
6
  import os
@@ -9,8 +9,8 @@ import tempfile
9
9
  import threading
10
10
 
11
11
 
12
- @register_core_tool
13
- class RunPowershellCommand(ToolBase):
12
+ @register_local_tool
13
+ class RunPowershellCommandTool(ToolBase):
14
14
  """
15
15
  Execute a non-interactive command using the PowerShell shell and capture live output.
16
16
  This tool explicitly invokes 'powershell.exe' (on Windows) or 'pwsh' (on other platforms if available).
@@ -1,12 +1,12 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.report_events import ReportAction
3
- from janito.plugins.tools.decorators import register_core_tool
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
4
  from janito.i18n import tr
5
5
  from janito.tools.loop_protection_decorator import protect_against_loops
6
6
 
7
7
 
8
- @register_core_tool
9
- class ShowImage(ToolBase):
8
+ @register_local_tool
9
+ class ShowImageTool(ToolBase):
10
10
  """Display an image inline in the terminal using the rich library.
11
11
 
12
12
  Args:
@@ -44,9 +44,7 @@ class ShowImage(ToolBase):
44
44
 
45
45
  path = expand_path(path)
46
46
  disp_path = display_path(path)
47
- self.report_action(
48
- tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ
49
- )
47
+ self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
50
48
 
51
49
  if not os.path.exists(path):
52
50
  msg = tr("❗ not found")
@@ -57,14 +55,9 @@ class ShowImage(ToolBase):
57
55
  console = Console()
58
56
  from rich.console import Console
59
57
  from rich.text import Text
60
-
61
58
  console = Console()
62
59
  img = PILImage.open(path)
63
- console.print(
64
- Text(
65
- f"Image: {disp_path} ({img.width}x{img.height})", style="bold green"
66
- )
67
- )
60
+ console.print(Text(f"Image: {disp_path} ({img.width}x{img.height})", style="bold green"))
68
61
  console.print(img)
69
62
  self.report_success(tr("✅ Displayed"))
70
63
  details = []
@@ -75,9 +68,7 @@ class ShowImage(ToolBase):
75
68
  if not preserve_aspect:
76
69
  details.append("preserve_aspect=False")
77
70
  info = ("; ".join(details)) if details else "auto-fit"
78
- return tr(
79
- "Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info
80
- )
71
+ return tr("Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info)
81
72
  except Exception as e:
82
73
  self.report_error(tr(" ❌ Error: {error}", error=e))
83
74
  return tr("Error displaying image: {error}", error=e)
@@ -1,10 +1,12 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
2
  from janito.report_events import ReportAction
3
+ from janito.tools.adapters.local.adapter import register_local_tool
3
4
  from janito.i18n import tr
4
5
  from janito.tools.loop_protection_decorator import protect_against_loops
5
6
  from typing import Sequence
6
7
 
7
8
 
9
+ @register_local_tool
8
10
  class ShowImageGridTool(ToolBase):
9
11
  """Display multiple images in a grid inline in the terminal using rich.
10
12
 
@@ -48,9 +50,7 @@ class ShowImageGridTool(ToolBase):
48
50
  if not paths:
49
51
  return tr("No images provided")
50
52
 
51
- self.report_action(
52
- tr("🖼️ Show image grid ({n} images)", n=len(paths)), ReportAction.READ
53
- )
53
+ self.report_action(tr("🖼️ Show image grid ({n} images)", n=len(paths)), ReportAction.READ)
54
54
 
55
55
  console = Console()
56
56
  images = []
@@ -63,9 +63,7 @@ class ShowImageGridTool(ToolBase):
63
63
  try:
64
64
  img = PILImage.open(fp)
65
65
  title = f"{display_path(fp)} ({img.width}x{img.height})"
66
- images.append(
67
- Panel.fit(title, title=display_path(fp), border_style="dim")
68
- )
66
+ images.append(Panel.fit(title, title=display_path(fp), border_style="dim"))
69
67
  shown += 1
70
68
  except Exception as e:
71
69
  self.report_warning(tr("⚠️ Skipped {p}: {e}", p=display_path(fp), e=e))
@@ -73,12 +71,6 @@ class ShowImageGridTool(ToolBase):
73
71
  if not images:
74
72
  return tr("No images could be displayed")
75
73
 
76
- # Render in columns (grid-like)
77
74
  console.print(Columns(images, equal=True, expand=True, columns=columns))
78
75
  self.report_success(tr("✅ Displayed {n} images", n=shown))
79
- return tr(
80
- "Displayed {shown}/{total} images in a {cols}x? grid",
81
- shown=shown,
82
- total=len(paths),
83
- cols=columns,
84
- )
76
+ return tr("Displayed {shown}/{total} images in a {cols}x? grid", shown=shown, total=len(paths), cols=columns)
@@ -0,0 +1,65 @@
1
+ """
2
+ Function-to-Tool adapter for core plugins.
3
+
4
+ This module provides a way to wrap function-based tools into proper ToolBase classes.
5
+ """
6
+
7
+ import inspect
8
+ from typing import Any, Dict, List, Optional, get_type_hints
9
+ from janito.tools.tool_base import ToolBase, ToolPermissions
10
+
11
+
12
+ class FunctionToolAdapter(ToolBase):
13
+ """Adapter that wraps a function into a ToolBase class."""
14
+
15
+ def __init__(self, func, tool_name: str = None, description: str = None):
16
+ super().__init__()
17
+ self._func = func
18
+ self.tool_name = tool_name or func.__name__
19
+ self._description = description or func.__doc__ or f"Tool: {self.tool_name}"
20
+ self.permissions = ToolPermissions(read=True, write=True, execute=True)
21
+
22
+ def run(self, **kwargs) -> Any:
23
+ """Execute the wrapped function."""
24
+ return self._func(**kwargs)
25
+
26
+ def get_signature(self) -> Dict[str, Any]:
27
+ """Get function signature for documentation."""
28
+ sig = inspect.signature(self._func)
29
+ type_hints = get_type_hints(self._func)
30
+
31
+ params = {}
32
+ for name, param in sig.parameters.items():
33
+ param_info = {
34
+ "type": str(type_hints.get(name, Any)),
35
+ "default": param.default if param.default != inspect.Parameter.empty else None,
36
+ "required": param.default == inspect.Parameter.empty,
37
+ }
38
+ params[name] = param_info
39
+
40
+ return {
41
+ "name": self.tool_name,
42
+ "description": self._description,
43
+ "parameters": params,
44
+ "return_type": str(type_hints.get("return", Any))
45
+ }
46
+
47
+
48
+ def create_function_tool(func, tool_name: str = None, description: str = None) -> type:
49
+ """
50
+ Create a ToolBase class from a function.
51
+
52
+ Args:
53
+ func: The function to wrap
54
+ tool_name: Optional custom tool name
55
+ description: Optional custom description
56
+
57
+ Returns:
58
+ A ToolBase subclass that wraps the function
59
+ """
60
+
61
+ class DynamicFunctionTool(FunctionToolAdapter):
62
+ def __init__(self):
63
+ super().__init__(func, tool_name, description)
64
+
65
+ return DynamicFunctionTool
@@ -1,140 +1,143 @@
1
1
  import functools
2
2
  import time
3
3
  import threading
4
- from typing import Any, Tuple
4
+ from typing import Callable, Any
5
+ from janito.tools.loop_protection import LoopProtection
6
+ from janito.tools.tool_use_tracker import normalize_path
7
+
5
8
 
6
9
  # Global tracking for decorator-based loop protection
7
10
  _decorator_call_tracker = {}
8
11
  _decorator_call_tracker_lock = threading.Lock()
9
12
 
10
13
 
11
- def _normalize_key_value(key_field: str, key_value: Any) -> Any:
12
- """Normalize key values, especially paths, so different representations map to the same key."""
13
- if key_value is None:
14
- return None
15
-
16
- try:
17
- if isinstance(key_field, str) and "path" in key_field.lower():
18
- from janito.tools.tool_use_tracker import (
19
- normalize_path as _norm, # reuse existing normalization
20
- )
21
-
22
- if isinstance(key_value, str):
23
- return _norm(key_value)
24
- if isinstance(key_value, (list, tuple)):
25
- return tuple(_norm(v) if isinstance(v, str) else v for v in key_value)
26
- except Exception:
27
- # Best-effort normalization – fall back to original value
28
- pass
29
-
30
- return key_value
31
-
32
-
33
- def _get_param_value(func, args, kwargs, key_field: str):
34
- """Extract the watched parameter value from args/kwargs using function signature."""
35
- if key_field in kwargs:
36
- return kwargs[key_field]
37
-
38
- # Handle positional arguments by mapping to parameter names
39
- if len(args) > 1: # args[0] is self
40
- import inspect
41
-
42
- try:
43
- sig = inspect.signature(func)
44
- param_names = list(sig.parameters.keys())
45
- if key_field in param_names:
46
- idx = param_names.index(key_field)
47
- if idx < len(args):
48
- return args[idx]
49
- except Exception:
50
- return None
51
-
52
- return None
53
-
54
-
55
- def _determine_operation_name(func, args, kwargs, key_field: str) -> str:
56
- """Build the operation name for rate limiting, optionally including a normalized key value."""
57
- if key_field:
58
- raw_value = _get_param_value(func, args, kwargs, key_field)
59
- if raw_value is not None:
60
- norm_value = _normalize_key_value(key_field, raw_value)
61
- return f"{func.__name__}_{norm_value}"
62
- return func.__name__
63
-
64
-
65
- def _check_and_record(
66
- op_name: str,
67
- current_time: float,
68
- time_window: float,
69
- max_calls: int,
70
- tool_instance: Any,
71
- ) -> Tuple[bool, str]:
72
- """Check loop limits for op_name and record the call. Returns (exceeded, message)."""
73
- with _decorator_call_tracker_lock:
74
- # Clean old timestamps
75
- if op_name in _decorator_call_tracker:
76
- _decorator_call_tracker[op_name] = [
77
- ts
78
- for ts in _decorator_call_tracker[op_name]
79
- if current_time - ts <= time_window
80
- ]
81
-
82
- # Check limit
83
- if (
84
- op_name in _decorator_call_tracker
85
- and len(_decorator_call_tracker[op_name]) >= max_calls
86
- ):
87
- if all(
88
- current_time - ts <= time_window
89
- for ts in _decorator_call_tracker[op_name]
90
- ):
91
- msg = (
92
- f"Loop protection: Too many {op_name} operations in a short time period "
93
- f"({max_calls} calls in {time_window}s). Please try a different approach or wait before retrying."
94
- )
95
- if hasattr(tool_instance, "report_error"):
96
- try:
97
- tool_instance.report_error(msg)
98
- except Exception:
99
- pass
100
- return True, msg
101
-
102
- # Record this call
103
- if op_name not in _decorator_call_tracker:
104
- _decorator_call_tracker[op_name] = []
105
- _decorator_call_tracker[op_name].append(current_time)
106
-
107
- return False, ""
108
-
109
-
110
14
  def protect_against_loops(
111
15
  max_calls: int = 5, time_window: float = 10.0, key_field: str = None
112
16
  ):
113
17
  """
114
18
  Decorator that adds loop protection to tool run methods.
115
19
 
116
- Tracks calls within a sliding time window and prevents excessive repeated operations.
117
- When key_field is provided, the limit is applied per unique normalized value of that parameter
118
- (e.g., per-path protection for file tools).
20
+ This decorator monitors tool executions and prevents excessive calls within
21
+ a configurable time window. It helps prevent infinite loops or excessive
22
+ resource consumption when tools are called repeatedly.
23
+
24
+ When the configured limits are exceeded, the decorator raises a RuntimeError
25
+ with a descriptive message. This exception will propagate up the call stack
26
+ unless caught by a try/except block in the calling code.
27
+
28
+ The decorator works by:
29
+ 1. Tracking the number of calls to the decorated function
30
+ 2. Checking if the calls exceed the configured limits
31
+ 3. Raising a RuntimeError if a potential loop is detected
32
+ 4. Allowing the method to proceed normally if the operation is safe
33
+
34
+ Args:
35
+ max_calls (int): Maximum number of calls allowed within the time window.
36
+ Defaults to 5 calls.
37
+ time_window (float): Time window in seconds for detecting excessive calls.
38
+ Defaults to 10.0 seconds.
39
+ key_field (str, optional): The parameter name to use for key matching instead of function name.
40
+ If provided, the decorator will track calls based on the value of this
41
+ parameter rather than the function name. Useful for tools that operate
42
+ on specific files or resources.
43
+
44
+ Example:
45
+ >>> @protect_against_loops(max_calls=3, time_window=5.0)
46
+ >>> def run(self, path: str) -> str:
47
+ >>> # Implementation here
48
+ >>> pass
49
+
50
+ >>> @protect_against_loops(max_calls=10, time_window=30.0)
51
+ >>> def run(self, file_paths: list) -> str:
52
+ >>> # Implementation here
53
+ >>> pass
54
+
55
+ >>> @protect_against_loops(max_calls=5, time_window=10.0, key_field='path')
56
+ >>> def run(self, path: str) -> str:
57
+ >>> # This will track calls per unique path value
58
+ >>> pass
59
+
60
+ Note:
61
+ When loop protection is triggered, a RuntimeError will be raised with a
62
+ descriptive message. This exception will propagate up the call stack
63
+ unless caught by a try/except block in the calling code.
119
64
  """
120
65
 
121
66
  def decorator(func):
122
67
  @functools.wraps(func)
123
68
  def wrapper(*args, **kwargs):
124
- # Methods should always have self; if not, execute directly.
69
+ # Get the tool instance (self)
125
70
  if not args:
71
+ # This shouldn't happen in normal usage as methods need self
126
72
  return func(*args, **kwargs)
127
73
 
128
- op_name = _determine_operation_name(func, args, kwargs, key_field)
129
- exceeded, msg = _check_and_record(
130
- op_name=op_name,
131
- current_time=time.time(),
132
- time_window=time_window,
133
- max_calls=max_calls,
134
- tool_instance=args[0],
135
- )
136
- if exceeded:
137
- return msg
74
+ # Determine the operation key
75
+ if key_field:
76
+ # Use the key_field parameter value as the operation key
77
+ key_value = None
78
+ if key_field in kwargs:
79
+ key_value = kwargs[key_field]
80
+ elif len(args) > 1:
81
+ # Handle positional arguments - need to map parameter names
82
+ import inspect
83
+
84
+ try:
85
+ sig = inspect.signature(func)
86
+ param_names = list(sig.parameters.keys())
87
+ if key_field in param_names:
88
+ field_index = param_names.index(key_field)
89
+ if field_index < len(args):
90
+ key_value = args[field_index]
91
+ except (ValueError, TypeError):
92
+ pass
93
+
94
+ if key_value is not None:
95
+ op_name = f"{func.__name__}_{key_value}"
96
+ else:
97
+ op_name = func.__name__
98
+ else:
99
+ # Use the function name as the operation name
100
+ op_name = func.__name__
101
+
102
+ # Check call limits
103
+ current_time = time.time()
104
+
105
+ with _decorator_call_tracker_lock:
106
+ # Clean up old entries outside the time window
107
+ if op_name in _decorator_call_tracker:
108
+ _decorator_call_tracker[op_name] = [
109
+ timestamp
110
+ for timestamp in _decorator_call_tracker[op_name]
111
+ if current_time - timestamp <= time_window
112
+ ]
113
+
114
+ # Check if we're exceeding the limit
115
+ if op_name in _decorator_call_tracker:
116
+ if len(_decorator_call_tracker[op_name]) >= max_calls:
117
+ # Check if all recent calls are within the time window
118
+ if all(
119
+ current_time - timestamp <= time_window
120
+ for timestamp in _decorator_call_tracker[op_name]
121
+ ):
122
+ # Return loop protection message as string instead of raising exception
123
+ error_msg = f"Loop protection: Too many {op_name} operations in a short time period ({max_calls} calls in {time_window}s). Please try a different approach or wait before retrying."
124
+
125
+ # Try to report the error through the tool's reporting mechanism
126
+ tool_instance = args[0] if args else None
127
+ if hasattr(tool_instance, "report_error"):
128
+ try:
129
+ tool_instance.report_error(error_msg)
130
+ except Exception:
131
+ pass # If reporting fails, we still return the message
132
+
133
+ return error_msg
134
+
135
+ # Record this call
136
+ if op_name not in _decorator_call_tracker:
137
+ _decorator_call_tracker[op_name] = []
138
+ _decorator_call_tracker[op_name].append(current_time)
139
+
140
+ # Proceed with the original function
138
141
  return func(*args, **kwargs)
139
142
 
140
143
  return wrapper
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: janito
3
+ Version: 3.7.0
4
+ Summary: A new Python package called janito.
5
+ Author-email: João Pinto <janito@ikignosis.org>
6
+ Project-URL: Homepage, https://github.com/ikignosis/janito
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: attrs==25.3.0
11
+ Requires-Dist: rich==14.0.0
12
+ Requires-Dist: pathspec==0.12.1
13
+ Requires-Dist: setuptools>=61.0
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: jinja2>=3.0.0
16
+ Requires-Dist: prompt_toolkit>=3.0.51
17
+ Requires-Dist: lxml>=5.4.0
18
+ Requires-Dist: requests>=2.32.4
19
+ Requires-Dist: bs4>=0.0.2
20
+ Requires-Dist: questionary>=2.0.1
21
+ Requires-Dist: openai>=1.68.0
22
+ Requires-Dist: Pillow>=10.0.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest; extra == "dev"
25
+ Requires-Dist: pre-commit; extra == "dev"
26
+ Requires-Dist: ruff==0.11.9; extra == "dev"
27
+ Requires-Dist: detect-secrets==1.4.0; extra == "dev"
28
+ Requires-Dist: codespell==2.4.1; extra == "dev"
29
+ Requires-Dist: black; extra == "dev"
30
+ Requires-Dist: questionary>=2.0.1; extra == "dev"
31
+ Requires-Dist: setuptools_scm>=8.0; extra == "dev"
32
+ Provides-Extra: coder
33
+ Requires-Dist: janito-coder; extra == "coder"
34
+ Dynamic: license-file
35
+
36
+ # nctl
37
+
38
+ ```bash
39
+ $ nctl --help
40
+ Usage: nctl <command>
41
+
42
+ Interact with Nine API resources. See https://docs.nineapis.ch for the full API docs.
43
+
44
+ Run "nctl <command> --help" for more information on a command.
45
+ ```
46
+
47
+ ## Setup
48
+
49
+ ```bash
50
+ # If you have go already installed
51
+ go install github.com/ninech/nctl@latest
52
+
53
+ # Homebrew
54
+ brew install ninech/taps/nctl
55
+
56
+ # Debian/Ubuntu
57
+ echo "deb [trusted=yes] https://repo.nine.ch/deb/ /" | sudo tee /etc/apt/sources.list.d/repo.nine.ch.list
58
+ sudo apt-get update
59
+ sudo apt-get install nctl
60
+
61
+ # Fedora/RHEL
62
+ cat <<EOF > /etc/yum.repos.d/repo.nine.ch.repo
63
+ [repo.nine.ch]
64
+ name=Nine Repo
65
+ baseurl=https://repo.nine.ch/yum/
66
+ enabled=1
67
+ gpgcheck=0
68
+ EOF
69
+ dnf install nctl
70
+
71
+ # Arch
72
+ # Install yay: https://github.com/Jguer/yay#binary
73
+ yay --version
74
+ yay -S nctl-bin
75
+ ```
76
+
77
+ For Windows users, nctl is also built for arm64 and amd64. You can download the
78
+ latest exe file from the [releases](https://github.com/ninech/nctl/releases) and
79
+ install it.
80
+
81
+ ## Getting started
82
+
83
+ * login to the API using `nctl auth login`
84
+ * run `nctl --help` to get a list of all available commands