janito 1.5.2__py3-none-any.whl → 1.6.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 (85) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +0 -1
  3. janito/agent/config.py +11 -10
  4. janito/agent/config_defaults.py +3 -2
  5. janito/agent/conversation.py +93 -119
  6. janito/agent/conversation_api.py +98 -0
  7. janito/agent/conversation_exceptions.py +12 -0
  8. janito/agent/conversation_tool_calls.py +22 -0
  9. janito/agent/conversation_ui.py +17 -0
  10. janito/agent/message_handler.py +8 -9
  11. janito/agent/{agent.py → openai_client.py} +48 -16
  12. janito/agent/openai_schema_generator.py +53 -37
  13. janito/agent/profile_manager.py +172 -0
  14. janito/agent/queued_message_handler.py +13 -14
  15. janito/agent/rich_live.py +32 -0
  16. janito/agent/rich_message_handler.py +64 -0
  17. janito/agent/runtime_config.py +6 -1
  18. janito/agent/{tools/tool_base.py → tool_base.py} +15 -8
  19. janito/agent/tool_registry.py +118 -132
  20. janito/agent/tools/__init__.py +41 -2
  21. janito/agent/tools/ask_user.py +43 -33
  22. janito/agent/tools/create_directory.py +18 -16
  23. janito/agent/tools/create_file.py +31 -36
  24. janito/agent/tools/fetch_url.py +23 -19
  25. janito/agent/tools/find_files.py +40 -36
  26. janito/agent/tools/get_file_outline.py +100 -22
  27. janito/agent/tools/get_lines.py +40 -32
  28. janito/agent/tools/gitignore_utils.py +9 -6
  29. janito/agent/tools/move_file.py +22 -13
  30. janito/agent/tools/py_compile_file.py +40 -0
  31. janito/agent/tools/remove_directory.py +34 -24
  32. janito/agent/tools/remove_file.py +22 -20
  33. janito/agent/tools/replace_file.py +51 -0
  34. janito/agent/tools/replace_text_in_file.py +69 -42
  35. janito/agent/tools/rich_live.py +9 -2
  36. janito/agent/tools/run_bash_command.py +155 -107
  37. janito/agent/tools/run_python_command.py +139 -0
  38. janito/agent/tools/search_files.py +51 -34
  39. janito/agent/tools/tools_utils.py +4 -2
  40. janito/agent/tools/utils.py +6 -2
  41. janito/cli/_print_config.py +42 -16
  42. janito/cli/_utils.py +1 -0
  43. janito/cli/arg_parser.py +182 -29
  44. janito/cli/config_commands.py +54 -22
  45. janito/cli/logging_setup.py +9 -3
  46. janito/cli/main.py +11 -10
  47. janito/cli/runner/__init__.py +2 -0
  48. janito/cli/runner/cli_main.py +148 -0
  49. janito/cli/runner/config.py +33 -0
  50. janito/cli/runner/formatting.py +12 -0
  51. janito/cli/runner/scan.py +44 -0
  52. janito/cli_chat_shell/__init__.py +0 -1
  53. janito/cli_chat_shell/chat_loop.py +71 -92
  54. janito/cli_chat_shell/chat_state.py +38 -0
  55. janito/cli_chat_shell/chat_ui.py +43 -0
  56. janito/cli_chat_shell/commands/__init__.py +45 -0
  57. janito/cli_chat_shell/commands/config.py +22 -0
  58. janito/cli_chat_shell/commands/history_reset.py +29 -0
  59. janito/cli_chat_shell/commands/session.py +48 -0
  60. janito/cli_chat_shell/commands/session_control.py +12 -0
  61. janito/cli_chat_shell/commands/system.py +73 -0
  62. janito/cli_chat_shell/commands/utility.py +29 -0
  63. janito/cli_chat_shell/config_shell.py +39 -10
  64. janito/cli_chat_shell/load_prompt.py +5 -2
  65. janito/cli_chat_shell/session_manager.py +24 -27
  66. janito/cli_chat_shell/ui.py +75 -40
  67. janito/rich_utils.py +15 -2
  68. janito/web/__main__.py +10 -2
  69. janito/web/app.py +88 -52
  70. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/METADATA +76 -11
  71. janito-1.6.0.dist-info/RECORD +81 -0
  72. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/WHEEL +1 -1
  73. janito/agent/rich_tool_handler.py +0 -43
  74. janito/agent/templates/system_instructions.j2 +0 -38
  75. janito/agent/tool_auto_imports.py +0 -5
  76. janito/agent/tools/append_text_to_file.py +0 -41
  77. janito/agent/tools/py_compile.py +0 -39
  78. janito/agent/tools/python_exec.py +0 -83
  79. janito/cli/runner.py +0 -137
  80. janito/cli_chat_shell/commands.py +0 -204
  81. janito/render_prompt.py +0 -13
  82. janito-1.5.2.dist-info/RECORD +0 -66
  83. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/entry_points.txt +0 -0
  84. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/licenses/LICENSE +0 -0
  85. {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/top_level.txt +0 -0
@@ -1,56 +1,60 @@
1
- from janito.agent.tools.tool_base import ToolBase
1
+ from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
-
3
+ from janito.agent.tools.tools_utils import pluralize
4
4
 
5
5
  import fnmatch
6
6
  from janito.agent.tools.gitignore_utils import filter_ignored
7
7
 
8
+
8
9
  @register_tool(name="find_files")
9
10
  class FindFilesTool(ToolBase):
11
+ """
12
+ Find files in one or more directories matching a pattern. Respects .gitignore.
10
13
 
11
- def call(self, directories: list[str], pattern: str, recursive: bool=False, max_results: int=100) -> str:
12
- """
13
- Find files in one or more directories matching a pattern. Respects .gitignore.
14
-
15
- Args:
16
- directories: List of directories to search in.
17
- pattern: File pattern to match. Uses Unix shell-style wildcards (fnmatch), e.g. '*.py', 'data_??.csv', '[a-z]*.txt'.
18
- recursive: Whether to search recursively in subdirectories. Defaults to False.
19
- max_results: Maximum number of results to return. Defaults to 100.
20
- Returns:
21
- Newline-separated list of matching file paths. Example:
14
+ Args:
15
+ directories (list[str]): List of directories to search in.
16
+ pattern (str): File pattern to match. Uses Unix shell-style wildcards (fnmatch), e.g. '*.py', 'data_??.csv', '[a-z]*.txt'.
17
+ recursive (bool, optional): Whether to search recursively in subdirectories. Defaults to False.
18
+ max_depth (int, optional): Maximum directory depth to search (0 = only top-level). If None, unlimited. Defaults to None.
19
+ Returns:
20
+ str: Newline-separated list of matching file paths. Example:
22
21
  "/path/to/file1.py\n/path/to/file2.py"
23
22
  "Warning: Empty file pattern provided. Operation skipped."
24
- """
23
+ """
24
+
25
+ def call(
26
+ self,
27
+ directories: list[str],
28
+ pattern: str,
29
+ recursive: bool = False,
30
+ max_depth: int = None,
31
+ ) -> str:
25
32
  import os
33
+
26
34
  if not pattern:
27
- self.report_warning("⚠️ Warning: Empty file pattern provided. Operation skipped.")
35
+ self.report_warning(
36
+ "⚠️ Warning: Empty file pattern provided. Operation skipped."
37
+ )
28
38
  return "Warning: Empty file pattern provided. Operation skipped."
29
39
  from janito.agent.tools.tools_utils import display_path
30
- matches = []
31
- rec = "recursively" if recursive else "non-recursively"
40
+
41
+ output = []
32
42
  for directory in directories:
33
43
  disp_path = display_path(directory)
34
44
  self.report_info(f"🔍 Searching for files '{pattern}' in '{disp_path}'")
35
45
  for root, dirs, files in os.walk(directory):
46
+ # Calculate depth
47
+ rel_path = os.path.relpath(root, directory)
48
+ depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
49
+ if max_depth is not None and depth > max_depth:
50
+ # Prune traversal
51
+ dirs[:] = []
52
+ continue
53
+ if not recursive and depth > 0:
54
+ # Only top-level if not recursive
55
+ break
36
56
  dirs, files = filter_ignored(root, dirs, files)
37
57
  for filename in fnmatch.filter(files, pattern):
38
- matches.append(os.path.join(root, filename))
39
- if len(matches) >= max_results:
40
- break
41
- if not recursive:
42
- break
43
- if len(matches) >= max_results:
44
- break
45
-
46
- warning = ""
47
- if len(matches) >= max_results:
48
- warning = "\n⚠️ Warning: Maximum result limit reached. Some matches may not be shown."
49
- suffix = " (Max Reached)"
50
- else:
51
- suffix = ""
52
- self.report_success(f" ✅ {len(matches)} {pluralize('file', len(matches))}{suffix}")
53
- return "\n".join(matches) + warning
54
-
55
-
56
- from janito.agent.tools.tools_utils import pluralize
58
+ output.append(os.path.join(root, filename))
59
+ self.report_success(f" ✅ {len(output)} {pluralize('file', len(output))} found")
60
+ return "\n".join(output)
@@ -1,39 +1,117 @@
1
- from janito.agent.tools.tool_base import ToolBase
1
+ from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
-
4
-
3
+ import os
4
+ import re
5
+ from typing import List
5
6
 
6
7
 
7
8
  @register_tool(name="get_file_outline")
8
9
  class GetFileOutlineTool(ToolBase):
9
- """Get an outline of a file's structure."""
10
- def call(self, file_path: str) -> str:
11
- """
12
- Get an outline of a file's structure.
10
+ """
11
+ Get an outline of a file's structure.
13
12
 
14
- Args:
15
- file_path (str): Path to the file.
13
+ Note:
14
+ The outline extraction for Python files is based on regular expression (regex) pattern matching for class and function definitions.
15
+ This approach may not capture all edge cases or non-standard code structures. For complex files, further examination or more advanced parsing may be required.
16
16
 
17
- Returns:
18
- str: Outline of the file's structure, starting with a summary line. Example:
19
- - "Outline: 5 items\nclass MyClass:\ndef my_function():\n..."
20
- - "Error reading file: <error message>"
21
- """
17
+ Args:
18
+ file_path (str): Path to the file.
19
+ Returns:
20
+ str: Outline of the file's structure, starting with a summary line. Example:
21
+ - "Outline: 5 items (python)\n| Type | Name | Start | End | Parent |\n|---------|-------------|-------|-----|----------|\n| class | MyClass | 1 | 20 | |\n| method | my_method | 3 | 10 | MyClass |\n| function| my_func | 22 | 30 | |\n..."
22
+ - "Outline: 100 lines (default)\nFile has 100 lines."
23
+ - "Error reading file: <error message>"
24
+ """
25
+
26
+ def call(self, file_path: str) -> str:
22
27
  from janito.agent.tools.tools_utils import display_path
28
+
23
29
  disp_path = display_path(file_path)
24
30
  self.report_info(f"📄 Getting outline for: {disp_path}")
25
31
 
26
32
  try:
27
- with open(file_path, 'r', encoding='utf-8') as f:
33
+ ext = os.path.splitext(file_path)[1].lower()
34
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
28
35
  lines = f.readlines()
29
- outline = [line.strip() for line in lines if line.strip()]
30
- num_items = len(outline)
31
-
32
- self.report_success(f" ✅ {num_items} {pluralize('item', num_items)}")
33
- return f"Outline: {num_items} items\n" + '\n'.join(outline)
36
+ if ext == ".py":
37
+ outline_items = self._parse_python_outline(lines)
38
+ outline_type = "python"
39
+ table = self._format_outline_table(outline_items)
40
+ self.report_success(f" {len(outline_items)} items ({outline_type})")
41
+ return f"Outline: {len(outline_items)} items ({outline_type})\n" + table
42
+ else:
43
+ outline_type = "default"
44
+ self.report_success(f"✅ {len(lines)} lines ({outline_type})")
45
+ return f"Outline: {len(lines)} lines ({outline_type})\nFile has {len(lines)} lines."
34
46
  except Exception as e:
35
- self.report_error(f" ❌ Error reading file: {e}")
47
+ self.report_error(f"❌ Error reading file: {e}")
36
48
  return f"Error reading file: {e}"
37
49
 
50
+ def _parse_python_outline(self, lines: List[str]):
51
+ # Regex for class, function, and method definitions
52
+ class_pat = re.compile(r"^(\s*)class\s+(\w+)")
53
+ func_pat = re.compile(r"^(\s*)def\s+(\w+)")
54
+ outline = []
55
+ stack = [] # (name, type, indent, start, parent)
56
+ for idx, line in enumerate(lines):
57
+ class_match = class_pat.match(line)
58
+ func_match = func_pat.match(line)
59
+ indent = len(line) - len(line.lstrip())
60
+ if class_match:
61
+ name = class_match.group(2)
62
+ parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
63
+ stack.append(("class", name, indent, idx + 1, parent))
64
+ elif func_match:
65
+ name = func_match.group(2)
66
+ parent = (
67
+ stack[-1][1]
68
+ if stack
69
+ and stack[-1][0] in ("class", "function")
70
+ and indent > stack[-1][2]
71
+ else ""
72
+ )
73
+ stack.append(("function", name, indent, idx + 1, parent))
74
+ # Pop stack if indentation decreases
75
+ while stack and indent < stack[-1][2]:
76
+ popped = stack.pop()
77
+ outline.append(
78
+ {
79
+ "type": (
80
+ popped[0]
81
+ if popped[0] != "function" or popped[3] == 1
82
+ else ("method" if popped[4] else "function")
83
+ ),
84
+ "name": popped[1],
85
+ # Add end line for popped item
86
+ "start": popped[3],
87
+ "end": idx,
88
+ "parent": popped[4],
89
+ }
90
+ )
91
+ # Pop any remaining items in the stack at EOF
92
+ for popped in stack:
93
+ outline.append(
94
+ {
95
+ "type": (
96
+ popped[0]
97
+ if popped[0] != "function" or popped[3] == 1
98
+ else ("method" if popped[4] else "function")
99
+ ),
100
+ "name": popped[1],
101
+ "start": popped[3],
102
+ "end": len(lines),
103
+ "parent": popped[4],
104
+ }
105
+ )
106
+ return outline
38
107
 
39
- from janito.agent.tools.tools_utils import pluralize
108
+ def _format_outline_table(self, outline_items):
109
+ if not outline_items:
110
+ return "No classes or functions found."
111
+ header = "| Type | Name | Start | End | Parent |\n|---------|-------------|-------|-----|----------|"
112
+ rows = []
113
+ for item in outline_items:
114
+ rows.append(
115
+ f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} |"
116
+ )
117
+ return header + "\n" + "\n".join(rows)
@@ -1,28 +1,28 @@
1
- from janito.agent.tools.tool_base import ToolBase
1
+ from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
-
3
+ from janito.agent.tools.tools_utils import pluralize
4
4
 
5
5
 
6
6
  @register_tool(name="get_lines")
7
7
  class GetLinesTool(ToolBase):
8
- """Read lines from a file. Returns specific lines if a range is provided, or the entire file if no range is given."""
9
- def call(self, file_path: str, from_line: int=None, to_line: int=None) -> str:
10
- """
11
- Get specific lines from a file.
8
+ """
9
+ Read lines from a file. Returns specific lines if a range is provided, or the entire file if no range is given. If both from_line and to_line are None, the entire file is returned in one call—no need to chunk or split requests when reading the full file.
12
10
 
13
- Args:
14
- file_path (str): Path to the file to read lines from.
15
- from_line (int, optional): Starting line number (1-based). If None, starts from the first line.
16
- to_line (int, optional): Ending line number (1-based). If None, reads to the end of the file. If both are None, the entire file is returned.
11
+ Args:
12
+ file_path (str): Path to the file to read lines from.
13
+ from_line (int, optional): Starting line number (1-based). If None, starts from the first line.
14
+ to_line (int, optional): Ending line number (1-based). If None, reads to the end of the file. If both are None, the entire file is returned.
15
+ Returns:
16
+ str: File content with a header indicating the file name and line range. Example:
17
+ - "---\nFile: /path/to/file.py | Lines: 1-10 (of 100)\n---\n<lines...>"
18
+ - "---\nFile: /path/to/file.py | All lines (total: 100)\n---\n<all lines...>"
19
+ - "Error reading file: <error message>"
20
+ - "❗ not found"
21
+ """
17
22
 
18
- Returns:
19
- str: File content with a header indicating the file name and line range. Example:
20
- - "---\nFile: /path/to/file.py | Lines: 1-10 (of 100)\n---\n<lines...>"
21
- - "---\nFile: /path/to/file.py | All lines (total: 100)\n---\n<all lines...>"
22
- - "Error reading file: <error message>"
23
- - "❗ not found"
24
- """
23
+ def call(self, file_path: str, from_line: int = None, to_line: int = None) -> str:
25
24
  from janito.agent.tools.tools_utils import display_path
25
+
26
26
  disp_path = display_path(file_path)
27
27
  if from_line and to_line:
28
28
  self.report_info(f"📄 Reading {disp_path} lines {from_line}-{to_line}")
@@ -30,39 +30,47 @@ class GetLinesTool(ToolBase):
30
30
  self.report_info(f"📄 Reading {disp_path} (all lines)")
31
31
 
32
32
  try:
33
- with open(file_path, 'r', encoding='utf-8') as f:
33
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
34
34
  lines = f.readlines()
35
- selected = lines[(from_line-1 if from_line else 0):(to_line if to_line else None)]
35
+ selected = lines[
36
+ (from_line - 1 if from_line else 0) : (to_line if to_line else None)
37
+ ]
36
38
  selected_len = len(selected)
37
39
  total_lines = len(lines)
38
40
  if from_line and to_line:
39
41
  requested = to_line - from_line + 1
40
42
  if selected_len < requested:
41
-
42
- self.report_success(f" ✅ {selected_len} {pluralize('line', selected_len)} (end)")
43
+ self.report_success(
44
+ f" ✅ {selected_len} {pluralize('line', selected_len)} (end at line {total_lines})"
45
+ )
43
46
  elif to_line < total_lines:
44
-
45
- self.report_success(f" ✅ {selected_len} {pluralize('line', selected_len)} ({total_lines - to_line} lines to end)")
47
+ self.report_success(
48
+ f" ✅ {selected_len} {pluralize('line', selected_len)} ({total_lines - to_line} lines to end)"
49
+ )
46
50
  else:
47
-
48
- self.report_success(f" ✅ {selected_len} {pluralize('line', selected_len)} (end)")
51
+ self.report_success(
52
+ f" ✅ {selected_len} {pluralize('line', selected_len)} (end at line {total_lines})"
53
+ )
49
54
  else:
50
-
51
- self.report_success(f" ✅ {selected_len} {pluralize('line', selected_len)} (full file)")
55
+ self.report_success(
56
+ f" ✅ {selected_len} {pluralize('line', selected_len)} (full file)"
57
+ )
52
58
  # Prepare header
53
59
  if from_line and to_line:
54
60
  header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (of {total_lines})\n---\n"
61
+ if to_line >= total_lines:
62
+ header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (end at line {total_lines})\n---\n"
55
63
  elif from_line:
56
64
  header = f"---\nFile: {disp_path} | Lines: {from_line}-END (of {total_lines})\n---\n"
65
+ header = f"---\nFile: {disp_path} | Lines: {from_line}-END (end at line {total_lines})\n---\n"
57
66
  else:
58
- header = f"---\nFile: {disp_path} | All lines (total: {total_lines})\n---\n"
59
- return header + ''.join(selected)
67
+ header = (
68
+ f"---\nFile: {disp_path} | All lines (total: {total_lines})\n---\n"
69
+ )
70
+ return header + "".join(selected)
60
71
  except Exception as e:
61
72
  if isinstance(e, FileNotFoundError):
62
73
  self.report_error("❗ not found")
63
74
  return "❗ not found"
64
75
  self.report_error(f" ❌ Error: {e}")
65
76
  return f"Error reading file: {e}"
66
-
67
-
68
- from janito.agent.tools.tools_utils import pluralize
@@ -5,15 +5,15 @@ from janito.agent.tools.utils import expand_path
5
5
  _spec = None
6
6
 
7
7
 
8
- def load_gitignore_patterns(gitignore_path='.gitignore'):
8
+ def load_gitignore_patterns(gitignore_path=".gitignore"):
9
9
  global _spec
10
10
  gitignore_path = expand_path(gitignore_path)
11
11
  if not os.path.exists(gitignore_path):
12
- _spec = pathspec.PathSpec.from_lines('gitwildmatch', [])
12
+ _spec = pathspec.PathSpec.from_lines("gitwildmatch", [])
13
13
  return _spec
14
- with open(gitignore_path, 'r', encoding='utf-8') as f:
14
+ with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
15
15
  lines = f.readlines()
16
- _spec = pathspec.PathSpec.from_lines('gitwildmatch', lines)
16
+ _spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
17
17
  return _spec
18
18
 
19
19
 
@@ -23,7 +23,7 @@ def is_ignored(path):
23
23
  if _spec is None:
24
24
  _spec = load_gitignore_patterns()
25
25
  # Normalize path to be relative and use forward slashes
26
- rel_path = os.path.relpath(path).replace(os.sep, '/')
26
+ rel_path = os.path.relpath(path).replace(os.sep, "/")
27
27
  return _spec.match_file(rel_path)
28
28
 
29
29
 
@@ -35,7 +35,10 @@ def filter_ignored(root, dirs, files, spec=None):
35
35
  spec = _spec
36
36
 
37
37
  def not_ignored(p):
38
- rel_path = os.path.relpath(os.path.join(root, p)).replace(os.sep, '/')
38
+ rel_path = os.path.relpath(os.path.join(root, p)).replace(os.sep, "/")
39
+ # Always ignore .git directory (like git does)
40
+ if rel_path == ".git" or rel_path.startswith(".git/"):
41
+ return False
39
42
  return not spec.match_file(rel_path)
40
43
 
41
44
  dirs[:] = [d for d in dirs if not_ignored(d)]
@@ -2,25 +2,30 @@ import os
2
2
  import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
4
  from janito.agent.tools.utils import expand_path, display_path
5
- from janito.agent.tools.tool_base import ToolBase
5
+ from janito.agent.tool_base import ToolBase
6
+
6
7
 
7
8
  @register_tool(name="move_file")
8
9
  class MoveFileTool(ToolBase):
9
10
  """
10
11
  Move a file from src_path to dest_path.
11
- """
12
- def call(self, src_path: str, dest_path: str, overwrite: bool = False) -> str:
13
- """
14
- Move a file from src_path to dest_path.
15
12
 
16
- Args:
17
- src_path (str): Source file path.
18
- dest_path (str): Destination file path.
19
- overwrite (bool, optional): Whether to overwrite if the destination exists. Defaults to False.
13
+ Args:
14
+ src_path (str): Source file path.
15
+ dest_path (str): Destination file path.
16
+ overwrite (bool, optional): Whether to overwrite if the destination exists. Defaults to False.
17
+ backup (bool, optional): If True, create a backup (.bak) of the destination before moving if it exists. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
18
+ Returns:
19
+ str: Status message indicating the result.
20
+ """
20
21
 
21
- Returns:
22
- str: Status message indicating the result.
23
- """
22
+ def call(
23
+ self,
24
+ src_path: str,
25
+ dest_path: str,
26
+ overwrite: bool = False,
27
+ backup: bool = False,
28
+ ) -> str:
24
29
  original_src = src_path
25
30
  original_dest = dest_path
26
31
  src = expand_path(src_path)
@@ -36,11 +41,15 @@ class MoveFileTool(ToolBase):
36
41
  return f"\u274c Source path '{disp_src}' is not a file."
37
42
  if os.path.exists(dest):
38
43
  if not overwrite:
39
- self.report_error(f"\u2757 Destination '{disp_dest}' exists and overwrite is False.")
44
+ self.report_error(
45
+ f"\u2757 Destination '{disp_dest}' exists and overwrite is False."
46
+ )
40
47
  return f"\u2757 Destination '{disp_dest}' already exists and overwrite is False."
41
48
  if os.path.isdir(dest):
42
49
  self.report_error(f"\u274c Destination '{disp_dest}' is a directory.")
43
50
  return f"\u274c Destination '{disp_dest}' is a directory."
51
+ if backup:
52
+ shutil.copy2(dest, dest + ".bak")
44
53
  try:
45
54
  shutil.move(src, dest)
46
55
  self.report_success(f"\u2705 File moved from '{disp_src}' to '{disp_dest}'")
@@ -0,0 +1,40 @@
1
+ from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tool_registry import register_tool
3
+
4
+ from typing import Optional
5
+ import py_compile
6
+
7
+
8
+ @register_tool(name="py_compile_file")
9
+ class PyCompileFileTool(ToolBase):
10
+ """
11
+ Validate a Python file by compiling it with py_compile.
12
+ Useful to validate python files after changing them, especially after import changes.
13
+
14
+ Args:
15
+ file_path (str): Path to the Python file to compile.
16
+ doraise (bool, optional): Whether to raise exceptions on compilation errors. Defaults to True.
17
+ Returns:
18
+ str: Compilation status message. Example:
19
+ - "✅ Compiled"
20
+ - "Compile error: <error message>"
21
+ - "Error: <error message>"
22
+ """
23
+
24
+ def call(self, file_path: str, doraise: Optional[bool] = True) -> str:
25
+ self.report_info(f"🛠️ Compiling Python file: {file_path}")
26
+
27
+ if not (file_path.endswith(".py") or file_path.endswith(".pyw")):
28
+ msg = f"Error: {file_path} is not a Python (.py/.pyw) file."
29
+ self.report_error(f" [py_compile_file] {msg}")
30
+ return msg
31
+ try:
32
+ py_compile.compile(file_path, doraise=doraise)
33
+ self.report_success(" ✅ Compiled")
34
+ return "✅ Compiled"
35
+ except py_compile.PyCompileError as e:
36
+ self.report_error(f" [py_compile_file] Compile error: {e}")
37
+ return f"Compile error: {e}"
38
+ except Exception as e:
39
+ self.report_error(f" [py_compile_file] Error: {e}")
40
+ return f"Error: {e}"
@@ -1,40 +1,50 @@
1
- from janito.agent.tools.tool_base import ToolBase
1
+ from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
+ from janito.agent.tools.tools_utils import pluralize
3
4
 
4
5
  import shutil
5
6
  import os
6
-
7
+ import zipfile
7
8
 
8
9
 
9
10
  @register_tool(name="remove_directory")
10
11
  class RemoveDirectoryTool(ToolBase):
11
- """Remove a directory. If recursive=False and directory not empty, raises error."""
12
- def call(self, directory: str, recursive: bool = False) -> str:
13
- """
14
- Remove a directory.
15
-
16
- Args:
17
- directory (str): Path to the directory to remove.
18
- recursive (bool, optional): Remove recursively if True. Defaults to False.
19
-
20
- Returns:
21
- str: Status message indicating result. Example:
22
- - "Directory removed: /path/to/dir"
23
- - "Error removing directory: <error message>"
24
- """
25
- self.report_info(f"🗃️ Removing directory: {directory} (recursive={recursive})")
26
-
12
+ """
13
+ Remove a directory. If recursive=False and directory not empty, raises error.
14
+
15
+ Args:
16
+ directory (str): Path to the directory to remove.
17
+ recursive (bool, optional): Remove recursively if True. Defaults to False.
18
+ backup (bool, optional): If True, create a backup (.bak.zip) before removing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
19
+ Returns:
20
+ str: Status message indicating result. Example:
21
+ - "Directory removed: /path/to/dir"
22
+ - "Error removing directory: <error message>"
23
+ """
24
+
25
+ def call(
26
+ self, directory: str, recursive: bool = False, backup: bool = False
27
+ ) -> str:
28
+ self.report_info(
29
+ f"\U0001f5c3\ufe0f Removing directory: {directory} (recursive={recursive})"
30
+ )
27
31
  try:
32
+ if backup and os.path.exists(directory) and os.path.isdir(directory):
33
+ backup_zip = directory.rstrip("/\\") + ".bak.zip"
34
+ with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
35
+ for root, dirs, files in os.walk(directory):
36
+ for file in files:
37
+ abs_path = os.path.join(root, file)
38
+ rel_path = os.path.relpath(
39
+ abs_path, os.path.dirname(directory)
40
+ )
41
+ zipf.write(abs_path, rel_path)
28
42
  if recursive:
29
43
  shutil.rmtree(directory)
30
44
  else:
31
45
  os.rmdir(directory)
32
-
33
- self.report_success(f"✅ 1 {pluralize('directory', 1)}")
46
+ self.report_success(f"\u2705 1 {pluralize('directory', 1)}")
34
47
  return f"Directory removed: {directory}"
35
48
  except Exception as e:
36
- self.report_error(f" Error removing directory: {e}")
49
+ self.report_error(f" \u274c Error removing directory: {e}")
37
50
  return f"Error removing directory: {e}"
38
-
39
-
40
- from janito.agent.tools.tools_utils import pluralize
@@ -1,38 +1,40 @@
1
1
  import os
2
+ import shutil
2
3
  from janito.agent.tool_registry import register_tool
3
4
  from janito.agent.tools.utils import expand_path, display_path
4
- from janito.agent.tools.tool_base import ToolBase
5
+ from janito.agent.tool_base import ToolBase
6
+
5
7
 
6
8
  @register_tool(name="remove_file")
7
9
  class RemoveFileTool(ToolBase):
8
10
  """
9
11
  Remove a file at the specified path.
10
- """
11
- def call(self, file_path: str) -> str:
12
- """
13
- Remove a file from the filesystem.
14
12
 
15
- Args:
16
- file_path (str): Path to the file to remove.
13
+ Args:
14
+ file_path (str): Path to the file to remove.
15
+ backup (bool, optional): If True, create a backup (.bak) before removing. Recommend using backup=True only in the first call to avoid redundant backups. Defaults to False.
16
+ Returns:
17
+ str: Status message indicating the result. Example:
18
+ - "\u2705 Successfully removed the file at ..."
19
+ - "\u2757 Cannot remove file: ..."
20
+ """
17
21
 
18
- Returns:
19
- str: Status message indicating the result. Example:
20
- - "✅ Successfully removed the file at ..."
21
- - "❗ Cannot remove file: ..."
22
- """
22
+ def call(self, file_path: str, backup: bool = False) -> str:
23
23
  original_path = file_path
24
24
  path = expand_path(file_path)
25
25
  disp_path = display_path(original_path, path)
26
26
  if not os.path.exists(path):
27
- self.report_error(f" File '{disp_path}' does not exist.")
28
- return f" File '{disp_path}' does not exist."
27
+ self.report_error(f"\u274c File '{disp_path}' does not exist.")
28
+ return f"\u274c File '{disp_path}' does not exist."
29
29
  if not os.path.isfile(path):
30
- self.report_error(f" Path '{disp_path}' is not a file.")
31
- return f" Path '{disp_path}' is not a file."
30
+ self.report_error(f"\u274c Path '{disp_path}' is not a file.")
31
+ return f"\u274c Path '{disp_path}' is not a file."
32
32
  try:
33
+ if backup:
34
+ shutil.copy2(path, path + ".bak")
33
35
  os.remove(path)
34
- self.report_success(f" File removed: '{disp_path}'")
35
- return f" Successfully removed the file at '{disp_path}'."
36
+ self.report_success(f"\u2705 File removed: '{disp_path}'")
37
+ return f"\u2705 Successfully removed the file at '{disp_path}'."
36
38
  except Exception as e:
37
- self.report_error(f" Error removing file: {e}")
38
- return f" Error removing file: {e}"
39
+ self.report_error(f"\u274c Error removing file: {e}")
40
+ return f"\u274c Error removing file: {e}"