janito 1.3.2__py3-none-any.whl → 1.4.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/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.2"
1
+ __version__ = "1.4.0"
@@ -4,6 +4,7 @@ You are an assistant for a analysis and development tool that operates on files
4
4
  directories using text-based operations.
5
5
 
6
6
  Provide a concise plan before calling any tool.
7
+ Always execute the plan immediately after presenting it, unless the user requests otherwise.
7
8
 
8
9
  <context>
9
10
  Always review `README_structure.txt` before conducting file-specific searches.
@@ -12,25 +13,20 @@ Explore files that might be relevant to the current task.
12
13
  </context>
13
14
 
14
15
  <analysis>
15
- When analyzing issues, you might want to look into the git history for clues.
16
+ In case of missing code or functions, look into the .bak files and check git diff/history for recent changes.
16
17
  </analysis>
17
18
 
18
19
  <editing>
19
20
  If in doubt during editing, use the `ask_user` function to get additional information; otherwise, proceed and inform the user of the decision made.
20
21
 
21
22
  When you need to make changes to a file, consider the following:
22
-
23
- - Use the `edit_file` tool when you want to update or fix specific text fragments within a file without altering the rest of its content. It is preferred over full file replacement when:
24
- - Only small, targeted changes are needed.
25
- - You want to avoid the risk of accidentally overwriting unrelated content.
26
- - The file is large, and rewriting the entire file would be inefficient.
27
- - You want to preserve formatting, comments, or code structure outside the replaced text.
28
-
23
+ - It is preferred to replace exact text occurrences over file overwriting.
29
24
  - When replacing files, review their current content before requesting the update.
30
25
  - When reorganizing, moving files, or functions, search for references in other files that might need to be updated accordingly.
31
26
  </editing>
32
27
 
33
28
  <finishing>
29
+ - When asked to commit and no message is provided, check the git diff and summarize the changes in the commit message.
34
30
  - Review the README content if there are user-exposed or public API changes.
35
31
  - Update `README_structure.txt` considering discovered, created, or modified files.
36
32
  </finishing>
@@ -1,102 +1,40 @@
1
1
  import os
2
2
  import json
3
3
  import traceback
4
+ from janito.agent.tools.tool_base import ToolBase
4
5
 
5
6
  class ToolHandler:
6
7
  _tool_registry = {}
7
8
 
8
- @classmethod
9
- def register_tool(cls, func):
10
- import inspect
11
- import typing
12
- from typing import get_origin, get_args
13
-
14
- name = func.__name__
15
- description = func.__doc__ or ""
16
-
17
- sig = inspect.signature(func)
18
- params_schema = {
19
- "type": "object",
20
- "properties": {},
21
- "required": []
22
- }
23
-
24
- for param_name, param in sig.parameters.items():
25
- if param.annotation is param.empty:
26
- raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
27
- param_type = param.annotation
28
- schema = {}
29
-
30
- # Handle typing.Optional, typing.List, typing.Literal, etc.
31
- origin = get_origin(param_type)
32
- args = get_args(param_type)
33
-
34
- if origin is typing.Union and type(None) in args:
35
- # Optional[...] type
36
- main_type = args[0] if args[1] is type(None) else args[1]
37
- origin = get_origin(main_type)
38
- args = get_args(main_type)
39
- param_type = main_type
40
- else:
41
- main_type = param_type
42
-
43
- if origin is list or origin is typing.List:
44
- item_type = args[0] if args else str
45
- item_schema = {"type": _pytype_to_json_type(item_type)}
46
- schema = {"type": "array", "items": item_schema}
47
- elif origin is typing.Literal:
48
- schema = {"type": _pytype_to_json_type(type(args[0])), "enum": list(args)}
49
- elif main_type == int:
50
- schema = {"type": "integer"}
51
- elif main_type == float:
52
- schema = {"type": "number"}
53
- elif main_type == bool:
54
- schema = {"type": "boolean"}
55
- elif main_type == dict:
56
- schema = {"type": "object"}
57
- elif main_type == list:
58
- schema = {"type": "array", "items": {"type": "string"}}
59
- else:
60
- schema = {"type": "string"}
61
-
62
- # Optionally add description if available in docstring (not implemented here)
63
- params_schema["properties"][param_name] = schema
64
- if param.default is param.empty:
65
- params_schema["required"].append(param_name)
66
-
67
- cls._tool_registry[name] = {
68
- "function": func,
69
- "description": description,
70
- "parameters": params_schema
71
- }
72
- return func
73
-
74
- def _pytype_to_json_type(pytype):
75
- import typing
76
- if pytype == int:
77
- return "integer"
78
- elif pytype == float:
79
- return "number"
80
- elif pytype == bool:
81
- return "boolean"
82
- elif pytype == dict:
83
- return "object"
84
- elif pytype == list or pytype == typing.List:
85
- return "array"
86
- else:
87
- return "string"
88
-
89
- class ToolHandler:
90
- _tool_registry = {}
9
+ def __init__(self, verbose=False, enable_tools=True):
10
+ self.verbose = verbose
11
+ self.tools = []
12
+ self.enable_tools = enable_tools
91
13
 
92
14
  @classmethod
93
- def register_tool(cls, func):
15
+ def register_tool(cls, tool=None, *, name: str = None):
16
+ """
17
+ Register a tool class derived from ToolBase.
18
+ Args:
19
+ tool: The tool class (must inherit from ToolBase).
20
+ name: Optional override for the tool name.
21
+ Raises:
22
+ TypeError: If the tool is not a subclass of ToolBase.
23
+ """
24
+ if tool is None:
25
+ return lambda t: cls.register_tool(t, name=name)
94
26
  import inspect
95
27
  import typing
96
28
  from typing import get_origin, get_args
97
29
 
98
- name = func.__name__
99
- description = func.__doc__ or ""
30
+ override_name = name
31
+ if not (isinstance(tool, type) and issubclass(tool, ToolBase)):
32
+ raise TypeError("Tool must be a class derived from ToolBase.")
33
+ instance = tool()
34
+ func = instance.call
35
+ default_name = tool.__name__
36
+ name = override_name or default_name
37
+ description = tool.__doc__ or func.__doc__ or ""
100
38
 
101
39
  sig = inspect.signature(func)
102
40
  params_schema = {
@@ -143,22 +81,30 @@ class ToolHandler:
143
81
  else:
144
82
  schema = {"type": "string"}
145
83
 
146
- # Optionally add description if available in docstring (not implemented here)
84
+ # Add description from call method docstring if available (Google-style Args parsing)
85
+ if func.__doc__:
86
+ import re
87
+ doc = func.__doc__
88
+ args_section = re.search(r"Args:\s*(.*?)(?:\n\s*\w|Returns:|$)", doc, re.DOTALL)
89
+ param_descs = {}
90
+ if args_section:
91
+ args_text = args_section.group(1)
92
+ for match in re.finditer(r"(\w+) \([^)]+\): ([^\n]+)", args_text):
93
+ pname, pdesc = match.groups()
94
+ param_descs[pname] = pdesc.strip()
95
+ if param_name in param_descs:
96
+ schema["description"] = param_descs[param_name]
147
97
  params_schema["properties"][param_name] = schema
148
98
  if param.default is param.empty:
149
99
  params_schema["required"].append(param_name)
150
100
 
101
+ # register the bound call function
151
102
  cls._tool_registry[name] = {
152
103
  "function": func,
153
104
  "description": description,
154
105
  "parameters": params_schema
155
106
  }
156
- return func
157
-
158
- def __init__(self, verbose=False, enable_tools=True):
159
- self.verbose = verbose
160
- self.tools = []
161
- self.enable_tools = enable_tools
107
+ return tool
162
108
 
163
109
  def register(self, func):
164
110
  self.tools.append(func)
@@ -194,6 +140,11 @@ class ToolHandler:
194
140
  print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
195
141
  import inspect
196
142
  sig = inspect.signature(func)
143
+ # Set progress callback on tool instance if possible
144
+ instance = None
145
+ if hasattr(func, '__self__') and isinstance(func.__self__, ToolBase):
146
+ instance = func.__self__
147
+ instance._progress_callback = on_progress
197
148
  if on_progress:
198
149
  on_progress({
199
150
  'event': 'start',
@@ -201,8 +152,6 @@ class ToolHandler:
201
152
  'tool': tool_call.function.name,
202
153
  'args': args
203
154
  })
204
- if 'on_progress' in sig.parameters and on_progress is not None:
205
- args['on_progress'] = on_progress
206
155
  try:
207
156
  result = func(**args)
208
157
  except Exception as e:
@@ -226,4 +175,22 @@ class ToolHandler:
226
175
  'args': args,
227
176
  'result': result
228
177
  })
178
+ # Clean up progress callback
179
+ if instance is not None:
180
+ instance._progress_callback = None
229
181
  return result
182
+
183
+ def _pytype_to_json_type(pytype):
184
+ import typing
185
+ if pytype == int:
186
+ return "integer"
187
+ elif pytype == float:
188
+ return "number"
189
+ elif pytype == bool:
190
+ return "boolean"
191
+ elif pytype == dict:
192
+ return "object"
193
+ elif pytype == list or pytype == typing.List:
194
+ return "array"
195
+ else:
196
+ return "string"
@@ -1,12 +1,9 @@
1
- from .ask_user import ask_user
2
- from .file_ops import create_file, create_directory, remove_file, move_file
3
- from .get_lines import get_lines
4
- from .replace_text_in_file import replace_text_in_file
5
- from .find_files import find_files
6
- from .run_bash_command import run_bash_command
7
- from .fetch_url import fetch_url
8
- from .python_exec import python_exec
9
- from .py_compile import py_compile_file
10
- from .search_files import search_files
11
- from .remove_directory import remove_directory
1
+ import importlib
2
+ import os
12
3
 
4
+ # Dynamically import all tool modules in this directory (except __init__.py and tool_base.py)
5
+ _tool_dir = os.path.dirname(__file__)
6
+ for fname in os.listdir(_tool_dir):
7
+ if fname.endswith('.py') and fname not in ('__init__.py', 'tool_base.py'):
8
+ modname = fname[:-3]
9
+ importlib.import_module(f'janito.agent.tools.{modname}')
@@ -1,64 +1,61 @@
1
- from janito.agent.tool_handler import ToolHandler
2
- from prompt_toolkit import PromptSession
3
- from prompt_toolkit.key_binding import KeyBindings
4
- from prompt_toolkit.enums import EditingMode
5
- from prompt_toolkit.formatted_text import HTML
6
- from prompt_toolkit.styles import Style
7
-
8
-
9
- @ToolHandler.register_tool
10
- def ask_user(question: str) -> str:
11
- """
12
- Ask the user a question and return their response.
13
-
14
- Args:
15
- question (str): The question to ask the user.
16
- """
17
- from rich import print as rich_print
18
- from rich.panel import Panel
19
-
20
- rich_print(Panel.fit(question, title="Question", style="cyan"))
21
-
22
- bindings = KeyBindings()
1
+ from janito.agent.tools.tool_base import ToolBase
2
+ from janito.agent.tools.rich_utils import print_info, print_success
3
+
4
+ class AskUserTool(ToolBase):
5
+ """Ask the user a question and return their response."""
6
+ def call(self, question: str) -> str:
7
+ from rich import print as rich_print
8
+ from rich.panel import Panel
9
+ from prompt_toolkit import PromptSession
10
+ from prompt_toolkit.key_binding import KeyBindings
11
+ from prompt_toolkit.enums import EditingMode
12
+ from prompt_toolkit.formatted_text import HTML
13
+ from prompt_toolkit.styles import Style
14
+
15
+ rich_print(Panel.fit(question, title="Question", style="cyan"))
16
+
17
+ bindings = KeyBindings()
18
+ mode = {'multiline': False}
19
+
20
+ @bindings.add('c-r')
21
+ def _(event):
22
+ pass
23
+
24
+ style = Style.from_dict({
25
+ 'bottom-toolbar': 'bg:#333333 #ffffff',
26
+ 'b': 'bold',
27
+ 'prompt': 'bold bg:#000080 #ffffff',
28
+ })
29
+
30
+ def get_toolbar():
31
+ if mode['multiline']:
32
+ return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
33
+ else:
34
+ return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
35
+
36
+ session = PromptSession(
37
+ multiline=False,
38
+ key_bindings=bindings,
39
+ editing_mode=EditingMode.EMACS,
40
+ bottom_toolbar=get_toolbar,
41
+ style=style
42
+ )
43
+
44
+ prompt_icon = HTML('<prompt>💬 </prompt>')
45
+
46
+ while True:
47
+ response = session.prompt(prompt_icon)
48
+ if not mode['multiline'] and response.strip() == '/multi':
49
+ mode['multiline'] = True
50
+ session.multiline = True
51
+ continue
52
+ elif mode['multiline'] and response.strip() == '/single':
53
+ mode['multiline'] = False
54
+ session.multiline = False
55
+ continue
56
+ else:
57
+ return response
23
58
 
24
- mode = {'multiline': False}
25
59
 
26
- @bindings.add('c-r')
27
- def _(event):
28
- # Disable reverse search
29
- pass
30
-
31
- style = Style.from_dict({
32
- 'bottom-toolbar': 'bg:#333333 #ffffff',
33
- 'b': 'bold',
34
- 'prompt': 'bold bg:#000080 #ffffff',
35
- })
36
-
37
- def get_toolbar():
38
- if mode['multiline']:
39
- return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
40
- else:
41
- return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
42
-
43
- session = PromptSession(
44
- multiline=False,
45
- key_bindings=bindings,
46
- editing_mode=EditingMode.EMACS,
47
- bottom_toolbar=get_toolbar,
48
- style=style
49
- )
50
-
51
- prompt_icon = HTML('<prompt>💬 </prompt>')
52
-
53
- while True:
54
- response = session.prompt(prompt_icon)
55
- if not mode['multiline'] and response.strip() == '/multi':
56
- mode['multiline'] = True
57
- session.multiline = True
58
- continue
59
- elif mode['multiline'] and response.strip() == '/single':
60
- mode['multiline'] = False
61
- session.multiline = False
62
- continue
63
- else:
64
- return response
60
+ from janito.agent.tool_handler import ToolHandler
61
+ ToolHandler.register_tool(AskUserTool, name="ask_user")
@@ -1,40 +1,35 @@
1
1
  import requests
2
- from typing import Optional, Callable
2
+ from typing import Optional
3
3
  from bs4 import BeautifulSoup
4
4
  from janito.agent.tool_handler import ToolHandler
5
5
  from janito.agent.tools.rich_utils import print_info, print_success, print_error
6
+ from janito.agent.tools.tool_base import ToolBase
6
7
 
7
- @ToolHandler.register_tool
8
- def fetch_url(url: str, search_strings: list[str] = None, on_progress: Optional[Callable[[dict], None]] = None) -> str:
9
- """
10
- Fetch the content of a web page and extract its text.
8
+ class FetchUrlTool(ToolBase):
9
+ """Fetch the content of a web page and extract its text."""
10
+ def call(self, url: str, search_strings: list[str] = None) -> str:
11
+ print_info(f"🌐 Fetching URL: {url} ... ")
12
+ response = requests.get(url, timeout=10)
13
+ response.raise_for_status()
14
+ self.update_progress(f"Fetched URL with status {response.status_code}")
15
+ soup = BeautifulSoup(response.text, 'html.parser')
16
+ text = soup.get_text(separator='\n')
11
17
 
12
- Args:
13
- url (str): The URL to fetch.
14
- search_strings (list[str], optional): List of strings to filter the extracted text around those strings.
15
- on_progress (callable, optional): Callback function for streaming progress updates.
16
- """
17
- print_info(f"\U0001F310 Fetching URL: {url} ... ")
18
- response = requests.get(url, timeout=10)
19
- response.raise_for_status()
20
- if on_progress:
21
- on_progress({'event': 'fetched', 'status_code': response.status_code})
22
- soup = BeautifulSoup(response.text, 'html.parser')
23
- text = soup.get_text(separator='\n')
18
+ if search_strings:
19
+ filtered = []
20
+ for s in search_strings:
21
+ idx = text.find(s)
22
+ if idx != -1:
23
+ start = max(0, idx - 200)
24
+ end = min(len(text), idx + len(s) + 200)
25
+ snippet = text[start:end]
26
+ filtered.append(snippet)
27
+ if filtered:
28
+ text = '\n...\n'.join(filtered)
29
+ else:
30
+ text = "No matches found for the provided search strings."
24
31
 
25
- if search_strings:
26
- filtered = []
27
- for s in search_strings:
28
- idx = text.find(s)
29
- if idx != -1:
30
- start = max(0, idx - 200)
31
- end = min(len(text), idx + len(s) + 200)
32
- snippet = text[start:end]
33
- filtered.append(snippet)
34
- if filtered:
35
- text = '\n...\n'.join(filtered)
36
- else:
37
- text = "No matches found for the provided search strings."
32
+ print_success("\u2705 Success")
33
+ return text
38
34
 
39
- print_success("\u2705 Success")
40
- return text
35
+ ToolHandler.register_tool(FetchUrlTool, name="fetch_url")
@@ -1,72 +1,114 @@
1
1
  import os
2
2
  import shutil
3
3
  from janito.agent.tool_handler import ToolHandler
4
- from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
4
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error
5
+ from janito.agent.tools.utils import expand_path, display_path
6
+ from janito.agent.tools.tool_base import ToolBase
5
7
 
6
- @ToolHandler.register_tool
7
- def create_file(path: str, content: str, overwrite: bool = False) -> str:
8
+ class CreateFileTool(ToolBase):
8
9
  """
9
10
  Create a new file or update an existing file with the given content.
10
-
11
- Args:
12
- path (str): Path to the file to create or update.
13
- content (str): Content to write to the file.
14
- overwrite (bool): Whether to overwrite the file if it exists.
15
11
  """
16
- updating = os.path.exists(path) and not os.path.isdir(path)
17
- if os.path.exists(path):
18
- if os.path.isdir(path):
19
- print_error("❌ Error: is a directory")
20
- return f"❌ Cannot create file: '{path}' is an existing directory."
21
- if not overwrite:
22
- print_error(f"❗ Error: file '{path}' exists and overwrite is False")
23
- return f" Cannot create file: '{path}' already exists and overwrite is False."
24
- if updating and overwrite:
25
- print_info(f"📝 Updating file: '{format_path(path)}' ... ")
26
- else:
27
- print_info(f"📝 Creating file: '{format_path(path)}' ... ")
28
- old_lines = None
29
- if updating and overwrite:
30
- with open(path, "r", encoding="utf-8") as f:
31
- old_lines = sum(1 for _ in f)
32
- with open(path, "w", encoding="utf-8") as f:
33
- f.write(content)
34
- print_success(" Success")
35
- if old_lines is not None:
12
+ def call(self, path: str, content: str, overwrite: bool = False) -> str:
13
+ original_path = path
14
+ path = expand_path(path)
15
+ updating = os.path.exists(path) and not os.path.isdir(path)
16
+ disp_path = display_path(original_path, path)
17
+ if os.path.exists(path):
18
+ if os.path.isdir(path):
19
+ print_error(f" Error: is a directory")
20
+ return f"❌ Cannot create file: '{disp_path}' is an existing directory."
21
+ if not overwrite:
22
+ print_error(f"❗ Error: file '{disp_path}' exists and overwrite is False")
23
+ return f" Cannot create file: '{disp_path}' already exists and overwrite is False."
24
+ if updating and overwrite:
25
+ print_info(f"📝 Updating file: '{disp_path}' ... ")
26
+ else:
27
+ print_info(f"📝 Creating file: '{disp_path}' ... ")
28
+ old_lines = None
29
+ if updating and overwrite:
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ old_lines = sum(1 for _ in f)
32
+ with open(path, "w", encoding="utf-8") as f:
33
+ f.write(content)
34
+ print_success("✅ Success")
35
+ if old_lines is not None:
36
+ new_lines = content.count('\n') + 1 if content else 0
37
+ return f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines)."
36
38
  new_lines = content.count('\n') + 1 if content else 0
37
- return f"✅ Successfully updated the file at '{path}' ({old_lines} > {new_lines} lines)."
38
- else:
39
- return f"✅ Successfully created the file at '{path}'."
39
+ return f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines)."
40
40
 
41
+ class CreateDirectoryTool(ToolBase):
42
+ """
43
+ Create a new directory at the specified path.
44
+ """
45
+ def call(self, path: str, overwrite: bool = False) -> str:
46
+ """
47
+ Create a new directory at the specified path.
48
+ Args:
49
+ path (str): Path to the directory to create.
50
+ overwrite (bool): Whether to remove the directory if it exists.
51
+ Returns:
52
+ str: Result message.
53
+ """
54
+ original_path = path
55
+ path = expand_path(path)
56
+ disp_path = display_path(original_path, path)
57
+ if os.path.exists(path):
58
+ if not os.path.isdir(path):
59
+ print_error(f"❌ Path '{disp_path}' exists and is not a directory.")
60
+ return f"❌ Path '{disp_path}' exists and is not a directory."
61
+ if not overwrite:
62
+ print_error(f"❗ Directory '{disp_path}' already exists and overwrite is False.")
63
+ return f"❗ Directory '{disp_path}' already exists and overwrite is False."
64
+ # Remove existing directory if overwrite is True
65
+ shutil.rmtree(path)
66
+ print_info(f"🗑️ Removed existing directory: '{disp_path}'")
67
+ os.makedirs(path, exist_ok=True)
68
+ print_success(f"✅ Created directory: '{disp_path}'")
69
+ return f"✅ Successfully created directory at '{disp_path}'."
41
70
 
42
- @ToolHandler.register_tool
43
- def remove_file(path: str) -> str:
44
- print_info(f"🗑️ Removing file: '{format_path(path)}' ... ")
45
- os.remove(path)
46
- print_success("✅ Success")
47
- return f"✅ Successfully deleted the file at '{path}'."
71
+ class RemoveFileTool(ToolBase):
72
+ """
73
+ Remove a file at the specified path.
74
+ """
75
+ def call(self, path: str) -> str:
76
+ original_path = path
77
+ path = expand_path(path)
78
+ disp_path = display_path(original_path, path)
79
+ print_info(f"🗑️ Removing file: '{disp_path}' ... ")
80
+ os.remove(path)
81
+ print_success("✅ Success")
82
+ return f"✅ Successfully deleted the file at '{disp_path}'."
48
83
 
49
- @ToolHandler.register_tool
50
- def move_file(source_path: str, destination_path: str, overwrite: bool = False) -> str:
51
- print_info(f"🚚 Moving '{format_path(source_path)}' to '{format_path(destination_path)}' ... ")
52
- if not os.path.exists(source_path):
53
- print_error("❌ Error: source does not exist")
54
- return f"❌ Source path '{source_path}' does not exist."
55
- if os.path.exists(destination_path):
56
- if not overwrite:
57
- print_error("❌ Error: destination exists and overwrite is False")
58
- return f"❌ Destination path '{destination_path}' already exists. Use overwrite=True to replace it."
59
- if os.path.isdir(destination_path):
60
- shutil.rmtree(destination_path)
61
- else:
62
- os.remove(destination_path)
63
- shutil.move(source_path, destination_path)
64
- print_success("✅ Success")
65
- return f"✅ Successfully moved '{source_path}' to '{destination_path}'."
84
+ class MoveFileTool(ToolBase):
85
+ """
86
+ Move or rename a file from source to destination.
87
+ """
88
+ def call(self, source_path: str, destination_path: str, overwrite: bool = False) -> str:
89
+ orig_source = source_path
90
+ orig_dest = destination_path
91
+ source_path = expand_path(source_path)
92
+ destination_path = expand_path(destination_path)
93
+ disp_source = display_path(orig_source, source_path)
94
+ disp_dest = display_path(orig_dest, destination_path)
95
+ print_info(f"🚚 Moving '{disp_source}' to '{disp_dest}' ... ")
96
+ if not os.path.exists(source_path):
97
+ print_error(f"❌ Error: source does not exist")
98
+ return f"❌ Source path '{disp_source}' does not exist."
99
+ if os.path.exists(destination_path):
100
+ if not overwrite:
101
+ print_error(f"❗ Error: destination exists and overwrite is False")
102
+ return f"❗ Destination path '{disp_dest}' already exists and overwrite is False."
103
+ if os.path.isdir(destination_path):
104
+ print_error(f"❌ Error: destination is a directory")
105
+ return f"❌ Destination path '{disp_dest}' is an existing directory."
106
+ shutil.move(source_path, destination_path)
107
+ print_success("✅ Success")
108
+ return f"✅ Successfully moved '{disp_source}' to '{disp_dest}'."
66
109
 
67
- @ToolHandler.register_tool
68
- def create_directory(path: str) -> str:
69
- print_info(f"📁 Creating directory: '{format_path(path)}' ... ")
70
- os.makedirs(path, exist_ok=True)
71
- print_success("✅ Success")
72
- return f"✅ Directory '{path}' created successfully."
110
+ # register tools
111
+ ToolHandler.register_tool(CreateFileTool, name="create_file")
112
+ ToolHandler.register_tool(CreateDirectoryTool, name="create_directory")
113
+ ToolHandler.register_tool(RemoveFileTool, name="remove_file")
114
+ ToolHandler.register_tool(MoveFileTool, name="move_file")