janito 2.24.1__py3-none-any.whl → 2.26.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 (48) hide show
  1. janito/cli/chat_mode/session.py +2 -2
  2. janito/cli/chat_mode/shell/commands/unrestricted.py +40 -0
  3. janito/cli/cli_commands/list_plugins.py +32 -0
  4. janito/cli/core/getters.py +2 -0
  5. janito/cli/main_cli.py +6 -2
  6. janito/cli/single_shot_mode/handler.py +2 -2
  7. janito/config_manager.py +8 -0
  8. janito/exceptions.py +19 -1
  9. janito/plugins/base.py +53 -2
  10. janito/plugins/config.py +87 -0
  11. janito/plugins/discovery.py +32 -0
  12. janito/plugins/manager.py +56 -2
  13. janito/tools/adapters/local/adapter.py +8 -26
  14. janito/tools/adapters/local/ask_user.py +1 -1
  15. janito/tools/adapters/local/copy_file.py +3 -1
  16. janito/tools/adapters/local/create_directory.py +2 -2
  17. janito/tools/adapters/local/create_file.py +8 -4
  18. janito/tools/adapters/local/fetch_url.py +25 -22
  19. janito/tools/adapters/local/find_files.py +3 -2
  20. janito/tools/adapters/local/get_file_outline/core.py +3 -1
  21. janito/tools/adapters/local/get_file_outline/search_outline.py +1 -1
  22. janito/tools/adapters/local/move_file.py +3 -2
  23. janito/tools/adapters/local/open_html_in_browser.py +1 -1
  24. janito/tools/adapters/local/open_url.py +1 -1
  25. janito/tools/adapters/local/python_file_run.py +2 -0
  26. janito/tools/adapters/local/read_chart.py +61 -54
  27. janito/tools/adapters/local/read_files.py +4 -3
  28. janito/tools/adapters/local/remove_directory.py +2 -0
  29. janito/tools/adapters/local/remove_file.py +3 -3
  30. janito/tools/adapters/local/run_powershell_command.py +1 -0
  31. janito/tools/adapters/local/search_text/core.py +3 -2
  32. janito/tools/adapters/local/validate_file_syntax/core.py +3 -1
  33. janito/tools/adapters/local/view_file.py +3 -1
  34. janito/tools/loop_protection_decorator.py +64 -25
  35. janito/tools/path_utils.py +39 -0
  36. janito/tools/tools_adapter.py +68 -22
  37. {janito-2.24.1.dist-info → janito-2.26.0.dist-info}/METADATA +1 -1
  38. {janito-2.24.1.dist-info → janito-2.26.0.dist-info}/RECORD +47 -39
  39. janito-2.26.0.dist-info/top_level.txt +2 -0
  40. janito-coder/janito_coder/__init__.py +9 -0
  41. janito-coder/janito_coder/plugins/__init__.py +27 -0
  42. janito-coder/janito_coder/plugins/code_navigator.py +618 -0
  43. janito-coder/janito_coder/plugins/git_analyzer.py +273 -0
  44. janito-coder/pyproject.toml +347 -0
  45. janito-2.24.1.dist-info/top_level.txt +0 -1
  46. {janito-2.24.1.dist-info → janito-2.26.0.dist-info}/WHEEL +0 -0
  47. {janito-2.24.1.dist-info → janito-2.26.0.dist-info}/entry_points.txt +0 -0
  48. {janito-2.24.1.dist-info → janito-2.26.0.dist-info}/licenses/LICENSE +0 -0
@@ -116,12 +116,12 @@ class ChatSession:
116
116
  def _select_profile_and_role(self, args, role):
117
117
  profile = getattr(args, "profile", None) if args is not None else None
118
118
  role_arg = getattr(args, "role", None) if args is not None else None
119
- python_profile = getattr(args, "python", False) if args is not None else False
119
+ python_profile = getattr(args, "developer", False) if args is not None else False
120
120
  market_profile = getattr(args, "market", False) if args is not None else False
121
121
  profile_system_prompt = None
122
122
  no_tools_mode = False
123
123
 
124
- # Handle --python flag
124
+ # Handle --developer flag
125
125
  if python_profile and profile is None and role_arg is None:
126
126
  profile = "Developer with Python Tools"
127
127
 
@@ -0,0 +1,40 @@
1
+ """Unrestricted mode command for chat mode."""
2
+
3
+ from janito.cli.chat_mode.shell.commands.base import ShellCmdHandler
4
+ from janito.cli.console import shared_console
5
+
6
+
7
+ class UnrestrictedShellHandler(ShellCmdHandler):
8
+ """Toggle unrestricted mode (equivalent to -u CLI flag)."""
9
+
10
+ help_text = "Toggle unrestricted mode (disable path security and URL whitelist)"
11
+
12
+ def run(self):
13
+ """Execute the unrestricted command."""
14
+ if not self.shell_state:
15
+ shared_console.print("[red]Error: Shell state not available[/red]")
16
+ return
17
+
18
+ # Toggle unrestricted mode
19
+ current_unrestricted = getattr(self.shell_state, 'unrestricted_mode', False)
20
+ new_unrestricted = not current_unrestricted
21
+
22
+ # Update shell state
23
+ self.shell_state.unrestricted_mode = new_unrestricted
24
+
25
+ # Update tools adapter
26
+ if hasattr(self.shell_state, 'tools_adapter'):
27
+ setattr(self.shell_state.tools_adapter, 'unrestricted_paths', new_unrestricted)
28
+
29
+ # Update URL whitelist manager
30
+ from janito.tools.url_whitelist import get_url_whitelist_manager
31
+ whitelist_manager = get_url_whitelist_manager()
32
+ whitelist_manager.set_unrestricted_mode(new_unrestricted)
33
+
34
+ status = "enabled" if new_unrestricted else "disabled"
35
+ warning = " (DANGEROUS - no path or URL restrictions)" if new_unrestricted else ""
36
+
37
+ shared_console.print(
38
+ f"[bold {'red' if new_unrestricted else 'green'}]"
39
+ f"Unrestricted mode {status}{warning}[/bold {'red' if new_unrestricted else 'green'}]"
40
+ )
@@ -24,6 +24,38 @@ def handle_list_plugins(args: argparse.Namespace) -> None:
24
24
  print("Search paths:")
25
25
  print(f" - {os.getcwd()}/plugins")
26
26
  print(f" - {os.path.expanduser('~')}/.janito/plugins")
27
+ elif getattr(args, 'list_resources', False):
28
+ # List all resources from loaded plugins
29
+ manager = PluginManager()
30
+ all_resources = manager.list_all_resources()
31
+
32
+ if all_resources:
33
+ print("Plugin Resources:")
34
+ for plugin_name, resources in all_resources.items():
35
+ metadata = manager.get_plugin_metadata(plugin_name)
36
+ print(f"\n{plugin_name} v{metadata.version if metadata else 'unknown'}:")
37
+
38
+ # Group resources by type
39
+ tools = [r for r in resources if r['type'] == 'tool']
40
+ commands = [r for r in resources if r['type'] == 'command']
41
+ configs = [r for r in resources if r['type'] == 'config']
42
+
43
+ if tools:
44
+ print(" Tools:")
45
+ for tool in tools:
46
+ print(f" - {tool['name']}: {tool['description']}")
47
+
48
+ if commands:
49
+ print(" Commands:")
50
+ for cmd in commands:
51
+ print(f" - {cmd['name']}: {cmd['description']}")
52
+
53
+ if configs:
54
+ print(" Configuration:")
55
+ for config in configs:
56
+ print(f" - {config['name']}: {config['description']}")
57
+ else:
58
+ print("No plugins loaded")
27
59
  else:
28
60
  # List loaded plugins
29
61
  manager = PluginManager()
@@ -26,6 +26,7 @@ GETTER_KEYS = [
26
26
  "list_providers_region",
27
27
  "list_plugins",
28
28
  "list_plugins_available",
29
+ "list_resources",
29
30
  ]
30
31
 
31
32
 
@@ -55,6 +56,7 @@ def handle_getter(args, config_mgr=None):
55
56
  "region_info": partial(handle_region_info, args),
56
57
  "list_providers_region": partial(handle_list_providers_region, args),
57
58
  "list_plugins": partial(handle_list_plugins, args),
59
+ "list_resources": partial(handle_list_plugins, args),
58
60
  }
59
61
  for arg in GETTER_KEYS:
60
62
  if getattr(args, arg, False) and arg in GETTER_DISPATCH:
janito/cli/main_cli.py CHANGED
@@ -38,7 +38,7 @@ definition = [
38
38
  },
39
39
  ),
40
40
  (
41
- ["--python"],
41
+ ["--developer"],
42
42
  {
43
43
  "action": "store_true",
44
44
  "help": "Start with the Python developer profile (equivalent to --profile 'Developer with Python Tools')",
@@ -230,6 +230,10 @@ definition = [
230
230
  ["--list-plugins-available"],
231
231
  {"action": "store_true", "help": "List all available plugins"},
232
232
  ),
233
+ (
234
+ ["--list-resources"],
235
+ {"action": "store_true", "help": "List all resources (tools, commands, config) from loaded plugins"},
236
+ ),
233
237
  ]
234
238
 
235
239
  MODIFIER_KEYS = [
@@ -237,7 +241,7 @@ MODIFIER_KEYS = [
237
241
  "model",
238
242
  "role",
239
243
  "profile",
240
- "python",
244
+ "developer",
241
245
  "market",
242
246
  "system",
243
247
  "temperature",
@@ -24,9 +24,9 @@ class PromptHandler:
24
24
  self.llm_driver_config = llm_driver_config
25
25
  self.role = role
26
26
  # Instantiate agent together with prompt handler using the shared helper
27
- # Handle --python and --market flags for single shot mode
27
+ # Handle --developer and --market flags for single shot mode
28
28
  profile = getattr(args, "profile", None)
29
- if profile is None and getattr(args, "python", False):
29
+ if profile is None and getattr(args, "developer", False):
30
30
  profile = "Developer with Python Tools"
31
31
  if profile is None and getattr(args, "market", False):
32
32
  profile = "Market Analyst"
janito/config_manager.py CHANGED
@@ -64,6 +64,14 @@ class ConfigManager:
64
64
  plugin_manager.load_plugins_from_config({"plugins": plugins_config})
65
65
  except Exception as e:
66
66
  print(f"Warning: Failed to load plugins from config: {e}")
67
+ else:
68
+ # Try loading from user config directory
69
+ try:
70
+ from janito.plugins.manager import PluginManager
71
+ plugin_manager = PluginManager()
72
+ plugin_manager.load_plugins_from_user_config()
73
+ except Exception as e:
74
+ print(f"Warning: Failed to load plugins from user config: {e}")
67
75
 
68
76
  # Load disabled tools from config - skip during startup to avoid circular imports
69
77
  # This will be handled by the CLI when needed
janito/exceptions.py CHANGED
@@ -9,7 +9,25 @@ class ToolCallException(Exception):
9
9
  self.error = error
10
10
  self.arguments = arguments
11
11
  self.original_exception = exception
12
- super().__init__(f"ToolCallException: {tool_name}: {error}")
12
+
13
+ # Build detailed error message
14
+ details = []
15
+ details.append(f"ToolCallException: {tool_name}: {error}")
16
+
17
+ if arguments is not None:
18
+ details.append(f"Arguments received: {arguments}")
19
+ if isinstance(arguments, dict):
20
+ details.append("Parameters:")
21
+ for key, value in arguments.items():
22
+ details.append(f" {key}: {repr(value)} ({type(value).__name__})")
23
+ elif isinstance(arguments, (list, tuple)):
24
+ details.append(f"Positional arguments: {arguments}")
25
+ for i, value in enumerate(arguments):
26
+ details.append(f" [{i}]: {repr(value)} ({type(value).__name__})")
27
+ else:
28
+ details.append(f"Single argument: {repr(arguments)} ({type(arguments).__name__})")
29
+
30
+ super().__init__("\n".join(details))
13
31
 
14
32
 
15
33
  class MissingProviderSelectionException(Exception):
janito/plugins/base.py CHANGED
@@ -4,7 +4,7 @@ Base classes for janito plugins.
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
  from dataclasses import dataclass
7
- from typing import Dict, Any, List, Optional, Type
7
+ from typing import Dict, Any, List, Optional, Type, Union
8
8
  from janito.tools.tool_base import ToolBase
9
9
 
10
10
 
@@ -24,6 +24,15 @@ class PluginMetadata:
24
24
  self.dependencies = []
25
25
 
26
26
 
27
+ @dataclass
28
+ class PluginResource:
29
+ """Represents a resource provided by a plugin."""
30
+ name: str
31
+ type: str # "tool", "command", "config"
32
+ description: str
33
+ schema: Optional[Dict[str, Any]] = None
34
+
35
+
27
36
  class Plugin(ABC):
28
37
  """
29
38
  Base class for all janito plugins.
@@ -90,4 +99,46 @@ class Plugin(ABC):
90
99
  Returns:
91
100
  True if configuration is valid
92
101
  """
93
- return True
102
+ return True
103
+
104
+ def get_resources(self) -> List[PluginResource]:
105
+ """
106
+ Return a list of resources provided by this plugin.
107
+
108
+ Returns:
109
+ List of PluginResource objects describing the resources
110
+ """
111
+ resources = []
112
+
113
+ # Add tools as resources
114
+ for tool_class in self.get_tools():
115
+ tool_instance = tool_class()
116
+ tool_name = getattr(tool_instance, 'tool_name', tool_class.__name__)
117
+ tool_desc = getattr(tool_class, '__doc__', f"Tool: {tool_name}")
118
+ resources.append(PluginResource(
119
+ name=tool_name,
120
+ type="tool",
121
+ description=tool_desc or f"Tool provided by {self.metadata.name}"
122
+ ))
123
+
124
+ # Add commands as resources
125
+ commands = self.get_commands()
126
+ for cmd_name, cmd_handler in commands.items():
127
+ cmd_desc = getattr(cmd_handler, '__doc__', f"Command: {cmd_name}")
128
+ resources.append(PluginResource(
129
+ name=cmd_name,
130
+ type="command",
131
+ description=cmd_desc or f"Command provided by {self.metadata.name}"
132
+ ))
133
+
134
+ # Add config schema as resource
135
+ config_schema = self.get_config_schema()
136
+ if config_schema:
137
+ resources.append(PluginResource(
138
+ name=f"{self.metadata.name}_config",
139
+ type="config",
140
+ description=f"Configuration schema for {self.metadata.name} plugin",
141
+ schema=config_schema
142
+ ))
143
+
144
+ return resources
@@ -0,0 +1,87 @@
1
+ """
2
+ Configuration management for plugins using user directory.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional
9
+
10
+
11
+ def get_user_config_dir() -> Path:
12
+ """Get the user configuration directory."""
13
+ return Path.home() / ".janito"
14
+
15
+
16
+ def get_plugins_config_path() -> Path:
17
+ """Get the path to the plugins configuration file."""
18
+ return get_user_config_dir() / "plugins.json"
19
+
20
+
21
+ def load_plugins_config() -> Dict[str, Any]:
22
+ """
23
+ Load plugins configuration from user directory.
24
+
25
+ Returns:
26
+ Dict containing plugins configuration
27
+ """
28
+ config_path = get_plugins_config_path()
29
+
30
+ if not config_path.exists():
31
+ # Create default config if it doesn't exist
32
+ default_config = {
33
+ "plugins": {
34
+ "paths": [
35
+ str(Path.home() / ".janito" / "plugins"),
36
+ "./plugins"
37
+ ],
38
+ "load": {}
39
+ }
40
+ }
41
+
42
+ # Ensure directory exists
43
+ config_path.parent.mkdir(parents=True, exist_ok=True)
44
+
45
+ # Save default config
46
+ with open(config_path, 'w') as f:
47
+ json.dump(default_config, f, indent=2)
48
+
49
+ return default_config
50
+
51
+ try:
52
+ with open(config_path, 'r') as f:
53
+ return json.load(f)
54
+ except (json.JSONDecodeError, IOError) as e:
55
+ print(f"Warning: Failed to load plugins config from {config_path}: {e}")
56
+ return {"plugins": {"paths": [], "load": {}}}
57
+
58
+
59
+ def save_plugins_config(config: Dict[str, Any]) -> bool:
60
+ """
61
+ Save plugins configuration to user directory.
62
+
63
+ Args:
64
+ config: Configuration dict to save
65
+
66
+ Returns:
67
+ True if saved successfully
68
+ """
69
+ config_path = get_plugins_config_path()
70
+
71
+ try:
72
+ # Ensure directory exists
73
+ config_path.parent.mkdir(parents=True, exist_ok=True)
74
+
75
+ with open(config_path, 'w') as f:
76
+ json.dump(config, f, indent=2)
77
+ return True
78
+ except IOError as e:
79
+ print(f"Error: Failed to save plugins config to {config_path}: {e}")
80
+ return False
81
+
82
+
83
+ def get_user_plugins_dir() -> Path:
84
+ """Get the user plugins directory."""
85
+ plugins_dir = get_user_config_dir() / "plugins"
86
+ plugins_dir.mkdir(parents=True, exist_ok=True)
87
+ return plugins_dir
@@ -1,5 +1,26 @@
1
1
  """
2
2
  Plugin discovery utilities.
3
+
4
+ Plugins can be provided in several formats:
5
+
6
+ 1. Single Python file: A .py file containing a Plugin class
7
+ Example: plugins/my_plugin.py
8
+
9
+ 2. Python package directory: A directory with __init__.py or plugin.py
10
+ Example: plugins/my_plugin/__init__.py
11
+ Example: plugins/my_plugin/plugin.py
12
+
13
+ 3. Installed Python package: An installed package with a Plugin class
14
+ Example: pip install janito-plugin-example
15
+
16
+ 4. ZIP file: A .zip file containing a Python package structure
17
+ Example: plugins/my_plugin.zip (containing package structure)
18
+
19
+ The plugin discovery system searches these locations in order:
20
+ - Current working directory/plugins/
21
+ - ~/.janito/plugins/
22
+ - Python installation share/janito/plugins/
23
+ - Any additional paths specified via configuration
3
24
  """
4
25
 
5
26
  import os
@@ -19,6 +40,12 @@ def discover_plugins(plugin_name: str, search_paths: List[Path] = None) -> Optio
19
40
  """
20
41
  Discover and load a plugin by name.
21
42
 
43
+ Supports multiple plugin formats:
44
+ - Single .py files
45
+ - Python package directories
46
+ - Installed Python packages
47
+ - ZIP files containing packages
48
+
22
49
  Args:
23
50
  plugin_name: Name of the plugin to discover
24
51
  search_paths: List of directories to search for plugins
@@ -126,6 +153,11 @@ def list_available_plugins(search_paths: List[Path] = None) -> List[str]:
126
153
  """
127
154
  List all available plugins in search paths.
128
155
 
156
+ Scans for plugins in multiple formats:
157
+ - .py files (excluding __init__.py)
158
+ - Directories with __init__.py or plugin.py
159
+ - Any valid plugin structure in search paths
160
+
129
161
  Args:
130
162
  search_paths: List of directories to search for plugins
131
163
 
janito/plugins/manager.py CHANGED
@@ -12,6 +12,7 @@ import logging
12
12
 
13
13
  from .base import Plugin, PluginMetadata
14
14
  from .discovery import discover_plugins
15
+ from .config import load_plugins_config, get_user_plugins_dir
15
16
  from janito.tools.adapters.local import LocalToolsAdapter
16
17
 
17
18
  logger = logging.getLogger(__name__)
@@ -158,6 +159,14 @@ class PluginManager:
158
159
  else:
159
160
  self.load_plugin(plugin_name, plugin_config)
160
161
 
162
+ def load_plugins_from_user_config(self) -> None:
163
+ """
164
+ Load plugins from user configuration directory.
165
+ Uses ~/.janito/plugins.json instead of janito.json
166
+ """
167
+ config = load_plugins_config()
168
+ self.load_plugins_from_config(config)
169
+
161
170
  def reload_plugin(self, plugin_name: str) -> bool:
162
171
  """
163
172
  Reload a plugin.
@@ -180,6 +189,51 @@ class PluginManager:
180
189
  'metadata': plugin.metadata,
181
190
  'tools': [tool.__name__ for tool in plugin.get_tools()],
182
191
  'commands': list(plugin.get_commands().keys()),
183
- 'config': self.plugin_configs.get(name, {})
192
+ 'config': self.plugin_configs.get(name, {}),
193
+ 'resources': [
194
+ {
195
+ 'name': resource.name,
196
+ 'type': resource.type,
197
+ 'description': resource.description,
198
+ 'schema': resource.schema
199
+ }
200
+ for resource in plugin.get_resources()
201
+ ]
202
+ }
203
+ return info
204
+
205
+ def get_plugin_resources(self, plugin_name: str) -> List[Dict[str, Any]]:
206
+ """
207
+ Get resources provided by a specific plugin.
208
+
209
+ Args:
210
+ plugin_name: Name of the plugin
211
+
212
+ Returns:
213
+ List of resource dictionaries
214
+ """
215
+ plugin = self.plugins.get(plugin_name)
216
+ if not plugin:
217
+ return []
218
+
219
+ return [
220
+ {
221
+ 'name': resource.name,
222
+ 'type': resource.type,
223
+ 'description': resource.description,
224
+ 'schema': resource.schema
184
225
  }
185
- return info
226
+ for resource in plugin.get_resources()
227
+ ]
228
+
229
+ def list_all_resources(self) -> Dict[str, List[Dict[str, Any]]]:
230
+ """
231
+ List all resources from all loaded plugins.
232
+
233
+ Returns:
234
+ Dict mapping plugin names to their resources
235
+ """
236
+ all_resources = {}
237
+ for plugin_name in self.plugins:
238
+ all_resources[plugin_name] = self.get_plugin_resources(plugin_name)
239
+ return all_resources
@@ -140,18 +140,18 @@ class LocalToolsAdapter(ToolsAdapter):
140
140
  def execute_tool(self, name: str, **kwargs):
141
141
  """
142
142
  Execute a tool with proper error handling.
143
-
143
+
144
144
  This method extends the base execute_tool functionality by adding
145
145
  error handling for RuntimeError exceptions that may be raised by
146
146
  tools with loop protection decorators.
147
-
147
+
148
148
  Args:
149
149
  name: The name of the tool to execute
150
150
  **kwargs: Arguments to pass to the tool
151
-
151
+
152
152
  Returns:
153
153
  The result of the tool execution
154
-
154
+
155
155
  Raises:
156
156
  ToolCallException: If tool execution fails for any reason
157
157
  ValueError: If the tool is not found or not allowed
@@ -160,30 +160,12 @@ class LocalToolsAdapter(ToolsAdapter):
160
160
  tool = self.get_tool(name)
161
161
  if not tool:
162
162
  raise ValueError(f"Tool '{name}' not found or not allowed.")
163
-
163
+
164
164
  # Record tool usage
165
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
- )
166
+
167
+ # Execute the tool using execute_by_name which handles loop protection
168
+ return self.execute_by_name(name, arguments=kwargs)
187
169
 
188
170
  # ------------------------------------------------------------------
189
171
  # Convenience methods
@@ -32,7 +32,7 @@ class AskUserTool(ToolBase):
32
32
  permissions = ToolPermissions(read=True)
33
33
  tool_name = "ask_user"
34
34
 
35
- @protect_against_loops(max_calls=5, time_window=10.0)
35
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="question")
36
36
  def run(self, question: str) -> str:
37
37
 
38
38
  print() # Print an empty line before the question panel
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from janito.tools.path_utils import expand_path
2
3
  import shutil
3
4
  from typing import List, Union
4
5
  from janito.tools.adapters.local.adapter import register_local_tool
@@ -26,7 +27,8 @@ class CopyFileTool(ToolBase):
26
27
  tool_name = "copy_file"
27
28
 
28
29
  def run(self, sources: str, target: str, overwrite: bool = False) -> str:
29
- source_list = [src for src in sources.split() if src]
30
+ source_list = [expand_path(src) for src in sources.split() if src]
31
+ target = expand_path(target)
30
32
  messages = []
31
33
  if len(source_list) > 1:
32
34
  if not os.path.isdir(target):
@@ -5,6 +5,7 @@ from janito.tools.tool_base import ToolBase, ToolPermissions
5
5
  from janito.report_events import ReportAction
6
6
  from janito.i18n import tr
7
7
  import os
8
+ from janito.tools.path_utils import expand_path
8
9
 
9
10
 
10
11
  @register_local_tool
@@ -23,8 +24,7 @@ class CreateDirectoryTool(ToolBase):
23
24
  tool_name = "create_directory"
24
25
 
25
26
  def run(self, path: str) -> str:
26
- # path = expand_path(path)
27
- # Using path as is
27
+ path = expand_path(path)
28
28
  disp_path = display_path(path)
29
29
  self.report_action(
30
30
  tr("📁 Create directory '{disp_path}' ...", disp_path=disp_path),
@@ -1,11 +1,12 @@
1
1
  import os
2
+ from janito.tools.path_utils import expand_path
2
3
  from janito.tools.adapters.local.adapter import register_local_tool
3
4
 
4
5
  from janito.tools.tool_utils import display_path
5
6
  from janito.tools.tool_base import ToolBase, ToolPermissions
6
7
  from janito.report_events import ReportAction
7
8
  from janito.i18n import tr
8
-
9
+ from janito.tools.loop_protection_decorator import protect_against_loops
9
10
 
10
11
  from janito.tools.adapters.local.validate_file_syntax.core import validate_file_syntax
11
12
 
@@ -24,15 +25,18 @@ class CreateFileTool(ToolBase):
24
25
  - "✅ Successfully created the file at ..."
25
26
 
26
27
  Note: Syntax validation is automatically performed after this operation.
28
+
29
+ Security: This tool includes loop protection to prevent excessive file creation operations.
30
+ Maximum 5 calls per 10 seconds for the same file path.
27
31
  """
28
32
 
29
33
  permissions = ToolPermissions(write=True)
30
34
  tool_name = "create_file"
31
35
 
36
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
32
37
  def run(self, path: str, content: str, overwrite: bool = False) -> str:
33
- expanded_path = path # Using path as is
34
- disp_path = display_path(expanded_path)
35
- path = expanded_path
38
+ path = expand_path(path)
39
+ disp_path = display_path(path)
36
40
  if os.path.exists(path) and not overwrite:
37
41
  try:
38
42
  with open(path, "r", encoding="utf-8", errors="replace") as f: