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.
- janito/README.md +1 -4
- janito/cli/chat_mode/bindings.py +0 -50
- janito/cli/chat_mode/session.py +1 -12
- janito/cli/chat_mode/shell/commands/multi.py +0 -5
- janito/cli/chat_mode/shell/commands/security/allowed_sites.py +33 -47
- janito/cli/cli_commands/list_plugins.py +43 -52
- janito/cli/core/getters.py +0 -3
- janito/cli/core/model_guesser.py +24 -40
- janito/cli/main_cli.py +12 -9
- janito/cli/prompt_core.py +9 -47
- janito/cli/rich_terminal_reporter.py +2 -2
- janito/docs/GETTING_STARTED.md +1 -1
- janito/drivers/openai/driver.py +0 -1
- janito/drivers/zai/driver.py +0 -1
- janito/i18n/it.py +46 -46
- janito/llm/agent.py +16 -32
- janito/llm/auth_utils.py +5 -14
- janito/llm/driver.py +0 -8
- janito/plugins/__init__.py +12 -31
- janito/plugins/auto_loader.py +11 -12
- janito/plugins/auto_loader_fixed.py +11 -12
- janito/{plugin_system → plugins}/base.py +2 -5
- janito/plugins/builtin.py +1 -15
- janito/plugins/core_adapter.py +11 -89
- janito/plugins/core_loader.py +120 -0
- janito/plugins/core_loader_fixed.py +125 -0
- janito/plugins/discovery.py +5 -5
- janito/plugins/discovery_core.py +9 -14
- janito/plugins/manager.py +1 -1
- janito/providers/__init__.py +0 -1
- janito/providers/moonshot/model_info.py +11 -0
- janito/tools/__init__.py +7 -31
- janito/tools/adapters/__init__.py +1 -6
- janito/tools/adapters/local/__init__.py +70 -7
- janito/{plugins/tools → tools/adapters/local}/ask_user.py +3 -3
- janito/{plugins/tools → tools/adapters/local}/create_file.py +6 -6
- janito/{plugins/tools → tools/adapters/local}/fetch_url.py +3 -3
- janito/{plugins/tools → tools/adapters/local}/python_code_run.py +7 -23
- janito/{plugins/tools → tools/adapters/local}/python_command_run.py +5 -21
- janito/{plugins/tools → tools/adapters/local}/python_file_run.py +5 -21
- janito/{plugins/tools → tools/adapters/local}/replace_text_in_file.py +4 -4
- janito/{plugins/tools → tools/adapters/local}/run_bash_command.py +3 -3
- janito/{plugins/tools → tools/adapters/local}/run_powershell_command.py +3 -3
- janito/{plugins/tools → tools/adapters/local}/show_image.py +6 -15
- janito/{plugins/core/imagedisplay/tools → tools/adapters/local}/show_image_grid.py +5 -13
- janito/tools/function_adapter.py +65 -0
- janito/tools/loop_protection_decorator.py +117 -114
- janito-3.7.0.dist-info/METADATA +84 -0
- {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/RECORD +88 -157
- janito/cli/cli_commands/check_tools.py +0 -212
- janito/llm/cancellation_manager.py +0 -63
- janito/llm/enter_cancellation.py +0 -107
- janito/plugin_system/__init__.py +0 -10
- janito/plugin_system/core_loader.py +0 -217
- janito/plugin_system/core_loader_fixed.py +0 -225
- janito/plugins/core/__init__.py +0 -7
- janito/plugins/core/codeanalyzer/__init__.py +0 -43
- janito/plugins/core/filemanager/__init__.py +0 -124
- janito/plugins/core/filemanager/tools/create_file.py +0 -87
- janito/plugins/core/filemanager/tools/replace_text_in_file.py +0 -270
- janito/plugins/core/imagedisplay/__init__.py +0 -14
- janito/plugins/core/imagedisplay/plugin.py +0 -51
- janito/plugins/core/imagedisplay/tools/__init__.py +0 -1
- janito/plugins/core/imagedisplay/tools/show_image.py +0 -83
- janito/plugins/core/system/__init__.py +0 -23
- janito/plugins/core/system/tools/run_bash_command.py +0 -204
- janito/plugins/core/system/tools/run_powershell_command.py +0 -234
- janito/plugins/dev/__init__.py +0 -7
- janito/plugins/dev/pythondev/__init__.py +0 -37
- janito/plugins/dev/visualization/__init__.py +0 -23
- janito/plugins/example_plugin.py +0 -108
- janito/plugins/tools/__init__.py +0 -10
- janito/plugins/tools/copy_file.py +0 -87
- janito/plugins/tools/core_tools_plugin.py +0 -87
- janito/plugins/tools/create_directory.py +0 -70
- janito/plugins/tools/decorators.py +0 -19
- janito/plugins/tools/delete_text_in_file.py +0 -134
- janito/plugins/tools/find_files.py +0 -143
- janito/plugins/tools/get_file_outline/__init__.py +0 -7
- janito/plugins/tools/get_file_outline/core.py +0 -122
- janito/plugins/tools/get_file_outline/java_outline.py +0 -47
- janito/plugins/tools/get_file_outline/markdown_outline.py +0 -14
- janito/plugins/tools/get_file_outline/python_outline.py +0 -303
- janito/plugins/tools/get_file_outline/search_outline.py +0 -36
- janito/plugins/tools/move_file.py +0 -131
- janito/plugins/tools/open_html_in_browser.py +0 -51
- janito/plugins/tools/open_url.py +0 -37
- janito/plugins/tools/read_chart.py +0 -259
- janito/plugins/tools/read_files.py +0 -58
- janito/plugins/tools/remove_directory.py +0 -55
- janito/plugins/tools/remove_file.py +0 -58
- janito/plugins/tools/search_text/__init__.py +0 -7
- janito/plugins/tools/search_text/core.py +0 -205
- janito/plugins/tools/search_text/match_lines.py +0 -67
- janito/plugins/tools/search_text/pattern_utils.py +0 -73
- janito/plugins/tools/search_text/traverse_directory.py +0 -145
- janito/plugins/tools/show_image_grid.py +0 -85
- janito/plugins/tools/validate_file_syntax/__init__.py +0 -7
- janito/plugins/tools/validate_file_syntax/core.py +0 -114
- janito/plugins/tools/validate_file_syntax/css_validator.py +0 -35
- janito/plugins/tools/validate_file_syntax/html_validator.py +0 -100
- janito/plugins/tools/validate_file_syntax/jinja2_validator.py +0 -50
- janito/plugins/tools/validate_file_syntax/js_validator.py +0 -27
- janito/plugins/tools/validate_file_syntax/json_validator.py +0 -6
- janito/plugins/tools/validate_file_syntax/markdown_validator.py +0 -109
- janito/plugins/tools/validate_file_syntax/ps1_validator.py +0 -32
- janito/plugins/tools/validate_file_syntax/python_validator.py +0 -5
- janito/plugins/tools/validate_file_syntax/xml_validator.py +0 -11
- janito/plugins/tools/validate_file_syntax/yaml_validator.py +0 -6
- janito/plugins/tools/view_file.py +0 -172
- janito/plugins/ui/__init__.py +0 -7
- janito/plugins/ui/userinterface/__init__.py +0 -16
- janito/plugins/ui/userinterface/tools/ask_user.py +0 -110
- janito/plugins/web/__init__.py +0 -7
- janito/plugins/web/webtools/__init__.py +0 -33
- janito/plugins/web/webtools/tools/fetch_url.py +0 -458
- janito/providers/together/__init__.py +0 -1
- janito/providers/together/model_info.py +0 -69
- janito/providers/together/provider.py +0 -108
- janito/tools/cli_initializer.py +0 -88
- janito/tools/initialize.py +0 -70
- janito-3.5.1.dist-info/METADATA +0 -229
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/copy_file.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/create_directory.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/delete_text_in_file.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/find_files.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/core.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/java_outline.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/python_outline.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/get_file_outline/search_outline.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/move_file.py +0 -0
- /janito/{plugins/web/webtools/tools → tools/adapters/local}/open_html_in_browser.py +0 -0
- /janito/{plugins/web/webtools/tools → tools/adapters/local}/open_url.py +0 -0
- /janito/{plugins/dev/visualization/tools → tools/adapters/local}/read_chart.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/read_files.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/remove_directory.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/remove_file.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/__init__.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/core.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/match_lines.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/pattern_utils.py +0 -0
- /janito/{plugins/core/codeanalyzer/tools → tools/adapters/local}/search_text/traverse_directory.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/core.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/jinja2_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +0 -0
- /janito/{plugins/core/filemanager/tools → tools/adapters/local}/view_file.py +0 -0
- {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/WHEEL +0 -0
- {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/entry_points.txt +0 -0
- {janito-3.5.1.dist-info → janito-3.7.0.dist-info}/licenses/LICENSE +0 -0
- {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.
|
8
|
+
from janito.tools.adapters.local.adapter import register_local_tool
|
9
9
|
from janito.i18n import tr
|
10
10
|
|
11
11
|
|
12
|
-
@
|
13
|
-
class
|
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
|
-
|
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.
|
8
|
+
from janito.tools.adapters.local.adapter import register_local_tool
|
9
9
|
from janito.i18n import tr
|
10
10
|
|
11
11
|
|
12
|
-
@
|
13
|
-
class
|
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
|
-
|
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.
|
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.
|
7
|
+
from janito.tools.adapters.local.validate_file_syntax.core import validate_file_syntax
|
8
8
|
|
9
9
|
|
10
|
-
@
|
11
|
-
class
|
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.
|
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
|
-
@
|
13
|
-
class
|
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.
|
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
|
-
@
|
13
|
-
class
|
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.
|
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
|
-
@
|
9
|
-
class
|
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
|
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
|
-
|
117
|
-
|
118
|
-
|
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
|
-
#
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|