janito 1.10.0__py3-none-any.whl → 1.11.1__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 (57) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/conversation_api.py +178 -90
  3. janito/agent/conversation_ui.py +1 -1
  4. janito/agent/llm_conversation_history.py +12 -0
  5. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +19 -4
  6. janito/agent/tools/__init__.py +2 -0
  7. janito/agent/tools/create_directory.py +1 -1
  8. janito/agent/tools/create_file.py +1 -1
  9. janito/agent/tools/fetch_url.py +1 -1
  10. janito/agent/tools/find_files.py +26 -13
  11. janito/agent/tools/get_file_outline/core.py +1 -1
  12. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  13. janito/agent/tools/get_lines.py +92 -63
  14. janito/agent/tools/move_file.py +58 -32
  15. janito/agent/tools/open_url.py +31 -0
  16. janito/agent/tools/python_command_runner.py +85 -86
  17. janito/agent/tools/python_file_runner.py +85 -86
  18. janito/agent/tools/python_stdin_runner.py +87 -88
  19. janito/agent/tools/remove_directory.py +1 -1
  20. janito/agent/tools/remove_file.py +1 -1
  21. janito/agent/tools/replace_file.py +2 -2
  22. janito/agent/tools/replace_text_in_file.py +193 -149
  23. janito/agent/tools/run_bash_command.py +1 -1
  24. janito/agent/tools/run_powershell_command.py +4 -0
  25. janito/agent/tools/search_text/__init__.py +1 -0
  26. janito/agent/tools/search_text/core.py +176 -0
  27. janito/agent/tools/search_text/match_lines.py +58 -0
  28. janito/agent/tools/search_text/pattern_utils.py +65 -0
  29. janito/agent/tools/search_text/traverse_directory.py +132 -0
  30. janito/agent/tools/validate_file_syntax/core.py +41 -30
  31. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  32. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  33. janito/agent/tools_utils/gitignore_utils.py +25 -2
  34. janito/agent/tools_utils/utils.py +7 -1
  35. janito/cli/config_commands.py +112 -109
  36. janito/shell/main.py +51 -8
  37. janito/shell/session/config.py +83 -75
  38. janito/shell/ui/interactive.py +97 -73
  39. janito/termweb/static/editor.css +32 -29
  40. janito/termweb/static/editor.css.bak +140 -22
  41. janito/termweb/static/editor.html +12 -7
  42. janito/termweb/static/editor.html.bak +16 -11
  43. janito/termweb/static/editor.js +94 -40
  44. janito/termweb/static/editor.js.bak +97 -65
  45. janito/termweb/static/index.html +1 -2
  46. janito/termweb/static/index.html.bak +1 -1
  47. janito/termweb/static/termweb.css +1 -22
  48. janito/termweb/static/termweb.css.bak +6 -4
  49. janito/termweb/static/termweb.js +0 -6
  50. janito/termweb/static/termweb.js.bak +1 -2
  51. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/METADATA +1 -1
  52. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/RECORD +56 -51
  53. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/WHEEL +1 -1
  54. janito/agent/tools/search_text.py +0 -254
  55. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/entry_points.txt +0 -0
  56. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/licenses/LICENSE +0 -0
  57. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/top_level.txt +0 -0
@@ -2,114 +2,158 @@ import re
2
2
  from typing import List
3
3
 
4
4
 
5
- def parse_python_outline(lines: List[str]):
6
- class_pat = re.compile(r"^(\s*)class\s+(\w+)")
7
- func_pat = re.compile(r"^(\s*)def\s+(\w+)")
8
- assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
9
- main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
5
+ def handle_assignment(idx, assign_match, outline):
6
+ var_name = assign_match.group(2)
7
+ var_type = "const" if var_name.isupper() else "var"
8
+ outline.append(
9
+ {
10
+ "type": var_type,
11
+ "name": var_name,
12
+ "start": idx + 1,
13
+ "end": idx + 1,
14
+ "parent": "",
15
+ "docstring": "",
16
+ }
17
+ )
18
+
19
+
20
+ def handle_main(idx, outline):
21
+ outline.append(
22
+ {
23
+ "type": "main",
24
+ "name": "__main__",
25
+ "start": idx + 1,
26
+ "end": idx + 1,
27
+ "parent": "",
28
+ "docstring": "",
29
+ }
30
+ )
31
+
32
+
33
+ def close_stack_objects(idx, indent, stack, obj_ranges):
34
+ while stack and indent < stack[-1][2]:
35
+ popped = stack.pop()
36
+ obj_ranges.append((popped[0], popped[1], popped[3], idx, popped[4], popped[2]))
37
+
38
+
39
+ def close_last_top_obj(idx, last_top_obj, stack, obj_ranges):
40
+ if last_top_obj and last_top_obj in stack:
41
+ stack.remove(last_top_obj)
42
+ obj_ranges.append(
43
+ (
44
+ last_top_obj[0],
45
+ last_top_obj[1],
46
+ last_top_obj[3],
47
+ idx,
48
+ last_top_obj[4],
49
+ last_top_obj[2],
50
+ )
51
+ )
52
+ return None
53
+ return last_top_obj
54
+
55
+
56
+ def handle_class(idx, class_match, indent, stack, last_top_obj):
57
+ name = class_match.group(2)
58
+ parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
59
+ obj = ("class", name, indent, idx + 1, parent)
60
+ stack.append(obj)
61
+ if indent == 0:
62
+ last_top_obj = obj
63
+ return last_top_obj
64
+
65
+
66
+ def handle_function(idx, func_match, indent, stack, last_top_obj):
67
+ name = func_match.group(2)
68
+ parent = ""
69
+ for s in reversed(stack):
70
+ if s[0] == "class" and indent > s[2]:
71
+ parent = s[1]
72
+ break
73
+ obj = ("function", name, indent, idx + 1, parent)
74
+ stack.append(obj)
75
+ if indent == 0:
76
+ last_top_obj = obj
77
+ return last_top_obj
78
+
79
+
80
+ def process_line(idx, line, regexes, stack, obj_ranges, outline, last_top_obj):
81
+ class_pat, func_pat, assign_pat, main_pat = regexes
82
+ class_match = class_pat.match(line)
83
+ func_match = func_pat.match(line)
84
+ assign_match = assign_pat.match(line)
85
+ indent = len(line) - len(line.lstrip())
86
+ # If a new top-level class or function starts, close the previous one
87
+ if (class_match or func_match) and indent == 0 and last_top_obj:
88
+ last_top_obj = close_last_top_obj(idx, last_top_obj, stack, obj_ranges)
89
+ if class_match:
90
+ last_top_obj = handle_class(idx, class_match, indent, stack, last_top_obj)
91
+ elif func_match:
92
+ last_top_obj = handle_function(idx, func_match, indent, stack, last_top_obj)
93
+ elif assign_match and indent == 0:
94
+ handle_assignment(idx, assign_match, outline)
95
+ main_match = main_pat.match(line)
96
+ if main_match:
97
+ handle_main(idx, outline)
98
+ close_stack_objects(idx, indent, stack, obj_ranges)
99
+ return last_top_obj
100
+
101
+
102
+ def build_outline_entry(obj, lines, outline):
103
+ obj_type, name, start, end, parent, indent = obj
104
+ # Determine if this is a method
105
+ if obj_type == "function" and parent:
106
+ outline_type = "method"
107
+ elif obj_type == "function":
108
+ outline_type = "function"
109
+ else:
110
+ outline_type = obj_type
111
+ docstring = extract_docstring(lines, start, end)
112
+ outline.append(
113
+ {
114
+ "type": outline_type,
115
+ "name": name,
116
+ "start": start,
117
+ "end": end,
118
+ "parent": parent,
119
+ "docstring": docstring,
120
+ }
121
+ )
122
+
123
+
124
+ def process_lines(lines, regexes):
10
125
  outline = []
11
- stack = [] # (type, name, indent, start, parent)
12
- obj_ranges = [] # (type, name, start, end, parent, indent)
126
+ stack = []
127
+ obj_ranges = []
13
128
  last_top_obj = None
14
129
  for idx, line in enumerate(lines):
15
- class_match = class_pat.match(line)
16
- func_match = func_pat.match(line)
17
- assign_match = assign_pat.match(line)
18
- indent = len(line) - len(line.lstrip())
19
- # If a new top-level class or function starts, close the previous one
20
- if (class_match or func_match) and indent == 0 and last_top_obj:
21
- # Only close if still open
22
- if last_top_obj in stack:
23
- stack.remove(last_top_obj)
24
- obj_ranges.append(
25
- (
26
- last_top_obj[0],
27
- last_top_obj[1],
28
- last_top_obj[3],
29
- idx,
30
- last_top_obj[4],
31
- last_top_obj[2],
32
- )
33
- )
34
- last_top_obj = None
35
- if class_match:
36
- name = class_match.group(2)
37
- parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
38
- obj = ("class", name, indent, idx + 1, parent)
39
- stack.append(obj)
40
- if indent == 0:
41
- last_top_obj = obj
42
- elif func_match:
43
- name = func_match.group(2)
44
- parent = ""
45
- for s in reversed(stack):
46
- if s[0] == "class" and indent > s[2]:
47
- parent = s[1]
48
- break
49
- obj = ("function", name, indent, idx + 1, parent)
50
- stack.append(obj)
51
- if indent == 0:
52
- last_top_obj = obj
53
- elif assign_match and indent == 0:
54
- var_name = assign_match.group(2)
55
- var_type = "const" if var_name.isupper() else "var"
56
- outline.append(
57
- {
58
- "type": var_type,
59
- "name": var_name,
60
- "start": idx + 1,
61
- "end": idx + 1,
62
- "parent": "",
63
- "docstring": "",
64
- }
65
- )
66
- main_match = main_pat.match(line)
67
- if main_match:
68
- outline.append(
69
- {
70
- "type": "main",
71
- "name": "__main__",
72
- "start": idx + 1,
73
- "end": idx + 1,
74
- "parent": "",
75
- "docstring": "",
76
- }
77
- )
78
- while stack and indent < stack[-1][2]:
79
- popped = stack.pop()
80
- obj_ranges.append(
81
- (popped[0], popped[1], popped[3], idx, popped[4], popped[2])
82
- )
130
+ last_top_obj = process_line(
131
+ idx, line, regexes, stack, obj_ranges, outline, last_top_obj
132
+ )
83
133
  # Close any remaining open objects
84
134
  for popped in stack:
85
135
  obj_ranges.append(
86
136
  (popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
87
137
  )
138
+ return outline, obj_ranges
139
+
88
140
 
89
- # Now, extract docstrings for classes, functions, and methods
141
+ def build_outline(obj_ranges, lines, outline):
90
142
  for obj in obj_ranges:
91
- obj_type, name, start, end, parent, indent = obj
92
- # Determine if this is a method
93
- if obj_type == "function" and parent:
94
- outline_type = "method"
95
- elif obj_type == "function":
96
- outline_type = "function"
97
- else:
98
- outline_type = obj_type
99
- docstring = extract_docstring(lines, start, end)
100
- outline.append(
101
- {
102
- "type": outline_type,
103
- "name": name,
104
- "start": start,
105
- "end": end,
106
- "parent": parent,
107
- "docstring": docstring,
108
- }
109
- )
143
+ build_outline_entry(obj, lines, outline)
110
144
  return outline
111
145
 
112
146
 
147
+ def parse_python_outline(lines: List[str]):
148
+ class_pat = re.compile(r"^(\s*)class\s+(\w+)")
149
+ func_pat = re.compile(r"^(\s*)def\s+(\w+)")
150
+ assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
151
+ main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
152
+ regexes = (class_pat, func_pat, assign_pat, main_pat)
153
+ outline, obj_ranges = process_lines(lines, regexes)
154
+ return build_outline(obj_ranges, lines, outline)
155
+
156
+
113
157
  def extract_docstring(lines, start_idx, end_idx):
114
158
  """Extracts a docstring from lines[start_idx:end_idx] if present."""
115
159
  for i in range(start_idx, min(end_idx, len(lines))):
@@ -22,18 +22,34 @@ class GetLinesTool(ToolBase):
22
22
  - "---\nFile: /path/to/file.py | Lines: 1-10 (of 100)\n---\n<lines...>"
23
23
  - "---\nFile: /path/to/file.py | All lines (total: 100 (all))\n---\n<all lines...>"
24
24
  - "Error reading file: <error message>"
25
- - " not found"
25
+ - "\u2757 not found"
26
26
  """
27
27
 
28
28
  def run(self, file_path: str, from_line: int = None, to_line: int = None) -> str:
29
29
  from janito.agent.tools_utils.utils import display_path
30
30
 
31
31
  disp_path = display_path(file_path)
32
+ self._report_read_info(disp_path, from_line, to_line)
33
+ try:
34
+ lines = self._read_file_lines(file_path)
35
+ selected, selected_len, total_lines = self._select_lines(
36
+ lines, from_line, to_line
37
+ )
38
+ self._report_success(selected_len, from_line, to_line, total_lines)
39
+ header = self._format_header(
40
+ disp_path, from_line, to_line, selected_len, total_lines
41
+ )
42
+ return header + "".join(selected)
43
+ except Exception as e:
44
+ return self._handle_read_error(e)
45
+
46
+ def _report_read_info(self, disp_path, from_line, to_line):
47
+ """Report the info message for reading lines."""
32
48
  if from_line and to_line:
33
49
  self.report_info(
34
50
  ActionType.READ,
35
51
  tr(
36
- "📖 Reading file '{disp_path}' {from_line}-{to_line}",
52
+ "📖 Read file '{disp_path}' {from_line}-{to_line}",
37
53
  disp_path=disp_path,
38
54
  from_line=from_line,
39
55
  to_line=to_line,
@@ -42,79 +58,92 @@ class GetLinesTool(ToolBase):
42
58
  else:
43
59
  self.report_info(
44
60
  ActionType.READ,
45
- tr("📖 Reading file '{disp_path}'", disp_path=disp_path),
61
+ tr("📖 Read file '{disp_path}'", disp_path=disp_path),
46
62
  )
47
- try:
48
- with open(file_path, "r", encoding="utf-8", errors="replace") as f:
49
- lines = f.readlines()
50
- selected = lines[
51
- (from_line - 1 if from_line else 0) : (to_line if to_line else None)
52
- ]
53
- selected_len = len(selected)
54
- total_lines = len(lines)
55
- at_end = False
56
- if from_line and to_line:
57
- requested = to_line - from_line + 1
58
- if to_line >= total_lines or selected_len < requested:
59
- at_end = True
60
- if at_end:
61
- self.report_success(
62
- tr(
63
- " {selected_len} {line_word} (end)",
64
- selected_len=selected_len,
65
- line_word=pluralize("line", selected_len),
66
- )
67
- )
68
- elif to_line < total_lines:
69
- self.report_success(
70
- tr(
71
- " ✅ {selected_len} {line_word} ({remaining} to end)",
72
- selected_len=selected_len,
73
- line_word=pluralize("line", selected_len),
74
- remaining=total_lines - to_line,
75
- )
76
- )
77
- else:
63
+
64
+ def _read_file_lines(self, file_path):
65
+ """Read all lines from the file."""
66
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
67
+ return f.readlines()
68
+
69
+ def _select_lines(self, lines, from_line, to_line):
70
+ """Select the requested lines and return them with their count and total lines."""
71
+ selected = lines[
72
+ (from_line - 1 if from_line else 0) : (to_line if to_line else None)
73
+ ]
74
+ selected_len = len(selected)
75
+ total_lines = len(lines)
76
+ return selected, selected_len, total_lines
77
+
78
+ def _report_success(self, selected_len, from_line, to_line, total_lines):
79
+ """Report the success message after reading lines."""
80
+ if from_line and to_line:
81
+ requested = to_line - from_line + 1
82
+ at_end = to_line >= total_lines or selected_len < requested
83
+ if at_end:
78
84
  self.report_success(
79
85
  tr(
80
- " {selected_len} {line_word} (all)",
86
+ " \u2705 {selected_len} {line_word} (end)",
81
87
  selected_len=selected_len,
82
88
  line_word=pluralize("line", selected_len),
83
89
  )
84
90
  )
85
- if from_line and to_line:
86
- if to_line >= total_lines or selected_len < (to_line - from_line + 1):
87
- header = tr(
88
- "---\n{disp_path} {from_line}-{to_line} (end)\n---\n",
89
- disp_path=disp_path,
90
- from_line=from_line,
91
- to_line=to_line,
92
- )
93
- else:
94
- header = tr(
95
- "---\n{disp_path} {from_line}-{to_line} (of {total_lines})\n---\n",
96
- disp_path=disp_path,
97
- from_line=from_line,
98
- to_line=to_line,
99
- total_lines=total_lines,
91
+ elif to_line < total_lines:
92
+ self.report_success(
93
+ tr(
94
+ " \u2705 {selected_len} {line_word} ({remaining} to end)",
95
+ selected_len=selected_len,
96
+ line_word=pluralize("line", selected_len),
97
+ remaining=total_lines - to_line,
100
98
  )
101
- elif from_line:
102
- header = tr(
103
- "---\n{disp_path} {from_line}-END (of {total_lines})\n---\n",
99
+ )
100
+ else:
101
+ self.report_success(
102
+ tr(
103
+ " \u2705 {selected_len} {line_word} (all)",
104
+ selected_len=selected_len,
105
+ line_word=pluralize("line", selected_len),
106
+ )
107
+ )
108
+
109
+ def _format_header(self, disp_path, from_line, to_line, selected_len, total_lines):
110
+ """Format the header for the output."""
111
+ if from_line and to_line:
112
+ requested = to_line - from_line + 1
113
+ at_end = selected_len < requested or to_line >= total_lines
114
+ if at_end:
115
+ return tr(
116
+ "---\n{disp_path} {from_line}-{to_line} (end)\n---\n",
104
117
  disp_path=disp_path,
105
118
  from_line=from_line,
106
- total_lines=total_lines,
119
+ to_line=to_line,
107
120
  )
108
121
  else:
109
- header = tr(
110
- "---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
122
+ return tr(
123
+ "---\n{disp_path} {from_line}-{to_line} (of {total_lines})\n---\n",
111
124
  disp_path=disp_path,
125
+ from_line=from_line,
126
+ to_line=to_line,
112
127
  total_lines=total_lines,
113
128
  )
114
- return header + "".join(selected)
115
- except Exception as e:
116
- if isinstance(e, FileNotFoundError):
117
- self.report_error(tr("❗ not found"))
118
- return tr("❗ not found")
119
- self.report_error(tr(" ❌ Error: {error}", error=e))
120
- return tr("Error reading file: {error}", error=e)
129
+ elif from_line:
130
+ return tr(
131
+ "---\n{disp_path} {from_line}-END (of {total_lines})\n---\n",
132
+ disp_path=disp_path,
133
+ from_line=from_line,
134
+ total_lines=total_lines,
135
+ )
136
+ else:
137
+ return tr(
138
+ "---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
139
+ disp_path=disp_path,
140
+ total_lines=total_lines,
141
+ )
142
+
143
+ def _handle_read_error(self, e):
144
+ """Handle file read errors and report appropriately."""
145
+ if isinstance(e, FileNotFoundError):
146
+ self.report_error(tr("\u2757 not found"))
147
+ return tr("\u2757 not found")
148
+ self.report_error(tr(" \u274c Error: {error}", error=e))
149
+ return tr("Error reading file: {error}", error=e)
@@ -1,8 +1,6 @@
1
1
  import os
2
2
  import shutil
3
3
  from janito.agent.tool_registry import register_tool
4
-
5
- # from janito.agent.tools_utils.expand_path import expand_path
6
4
  from janito.agent.tools_utils.utils import display_path
7
5
  from janito.agent.tool_base import ToolBase
8
6
  from janito.agent.tools_utils.action_type import ActionType
@@ -32,16 +30,55 @@ class MoveFileTool(ToolBase):
32
30
  ) -> str:
33
31
  original_src = src_path
34
32
  original_dest = dest_path
35
- src = src_path # Using src_path as is
36
- dest = dest_path # Using dest_path as is
33
+ src = src_path
34
+ dest = dest_path
37
35
  disp_src = display_path(original_src)
38
36
  disp_dest = display_path(original_dest)
39
37
  backup_path = None
38
+
39
+ valid, is_src_file, is_src_dir, err_msg = self._validate_source(src, disp_src)
40
+ if not valid:
41
+ return err_msg
42
+
43
+ dest_result = self._handle_destination(dest, disp_dest, overwrite, backup)
44
+ if dest_result is not None:
45
+ backup_path, err_msg = dest_result
46
+ if err_msg:
47
+ return err_msg
48
+
49
+ try:
50
+ self.report_info(
51
+ ActionType.WRITE,
52
+ tr(
53
+ "📝 Moving from '{disp_src}' to '{disp_dest}' ...",
54
+ disp_src=disp_src,
55
+ disp_dest=disp_dest,
56
+ ),
57
+ )
58
+ shutil.move(src, dest)
59
+ self.report_success(tr("✅ Move complete."))
60
+ msg = tr("✅ Move complete.")
61
+ if backup_path:
62
+ msg += tr(
63
+ " (backup at {backup_disp})",
64
+ backup_disp=display_path(backup_path),
65
+ )
66
+ return msg
67
+ except Exception as e:
68
+ self.report_error(tr("❌ Error moving: {error}", error=e))
69
+ return tr("❌ Error moving: {error}", error=e)
70
+
71
+ def _validate_source(self, src, disp_src):
40
72
  if not os.path.exists(src):
41
73
  self.report_error(
42
74
  tr("❌ Source '{disp_src}' does not exist.", disp_src=disp_src)
43
75
  )
44
- return tr("❌ Source '{disp_src}' does not exist.", disp_src=disp_src)
76
+ return (
77
+ False,
78
+ False,
79
+ False,
80
+ tr("❌ Source '{disp_src}' does not exist.", disp_src=disp_src),
81
+ )
45
82
  is_src_file = os.path.isfile(src)
46
83
  is_src_dir = os.path.isdir(src)
47
84
  if not (is_src_file or is_src_dir):
@@ -51,10 +88,19 @@ class MoveFileTool(ToolBase):
51
88
  disp_src=disp_src,
52
89
  )
53
90
  )
54
- return tr(
55
- "❌ Source path '{disp_src}' is neither a file nor a directory.",
56
- disp_src=disp_src,
91
+ return (
92
+ False,
93
+ False,
94
+ False,
95
+ tr(
96
+ "❌ Source path '{disp_src}' is neither a file nor a directory.",
97
+ disp_src=disp_src,
98
+ ),
57
99
  )
100
+ return True, is_src_file, is_src_dir, None
101
+
102
+ def _handle_destination(self, dest, disp_dest, overwrite, backup):
103
+ backup_path = None
58
104
  if os.path.exists(dest):
59
105
  if not overwrite:
60
106
  self.report_error(
@@ -63,7 +109,7 @@ class MoveFileTool(ToolBase):
63
109
  disp_dest=disp_dest,
64
110
  )
65
111
  )
66
- return tr(
112
+ return None, tr(
67
113
  "❗ Destination '{disp_dest}' already exists and overwrite is False.",
68
114
  disp_dest=disp_dest,
69
115
  )
@@ -83,27 +129,7 @@ class MoveFileTool(ToolBase):
83
129
  self.report_error(
84
130
  tr("❌ Error removing destination before move: {error}", error=e)
85
131
  )
86
- return tr("❌ Error removing destination before move: {error}", error=e)
87
- try:
88
- self.report_info(
89
- ActionType.WRITE,
90
- tr(
91
- "📝 Moving from '{disp_src}' to '{disp_dest}' ...",
92
- disp_src=disp_src,
93
- disp_dest=disp_dest,
94
- ),
95
- )
96
- shutil.move(src, dest)
97
- self.report_success(tr("✅ Move complete."))
98
- msg = tr("✅ Move complete.")
99
- if backup_path:
100
- msg += tr(
101
- " (backup at {backup_disp})",
102
- backup_disp=display_path(
103
- original_dest + (".bak" if is_src_file else ".bak.zip")
104
- ),
132
+ return None, tr(
133
+ "❌ Error removing destination before move: {error}", error=e
105
134
  )
106
- return msg
107
- except Exception as e:
108
- self.report_error(tr("❌ Error moving: {error}", error=e))
109
- return tr("❌ Error moving: {error}", error=e)
135
+ return backup_path, None
@@ -0,0 +1,31 @@
1
+ import webbrowser
2
+ from janito.agent.tool_registry import register_tool
3
+ from janito.agent.tool_base import ToolBase
4
+ from janito.agent.tools_utils.action_type import ActionType
5
+ from janito.i18n import tr
6
+
7
+
8
+ @register_tool(name="open_url")
9
+ class OpenUrlTool(ToolBase):
10
+ """
11
+ Open the supplied URL in the default web browser.
12
+ Args:
13
+ url (str): The URL to open.
14
+ Returns:
15
+ str: Status message indicating the result.
16
+ """
17
+
18
+ def run(self, url: str) -> str:
19
+ if not url.strip():
20
+ self.report_warning(tr("ℹ️ Empty URL provided."))
21
+ return tr("Warning: Empty URL provided. Operation skipped.")
22
+ self.report_info(ActionType.READ, tr("🌐 Opening URL '{url}' ...", url=url))
23
+ try:
24
+ webbrowser.open(url)
25
+ except Exception as err:
26
+ self.report_error(
27
+ tr("❗ Error opening URL: {url}: {err}", url=url, err=str(err))
28
+ )
29
+ return tr("Warning: Error opening URL: {url}: {err}", url=url, err=str(err))
30
+ self.report_success(tr("✅ URL opened in browser: {url}", url=url))
31
+ return tr("URL opened in browser: {url}", url=url)