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 +1 -1
- janito/agent/templates/system_instructions.j2 +4 -8
- janito/agent/tool_handler.py +62 -95
- janito/agent/tools/__init__.py +8 -11
- janito/agent/tools/ask_user.py +59 -62
- janito/agent/tools/fetch_url.py +27 -32
- janito/agent/tools/file_ops.py +102 -60
- janito/agent/tools/find_files.py +25 -52
- janito/agent/tools/get_file_outline.py +21 -0
- janito/agent/tools/get_lines.py +32 -56
- janito/agent/tools/gitignore_utils.py +4 -1
- janito/agent/tools/py_compile.py +19 -22
- janito/agent/tools/python_exec.py +27 -20
- janito/agent/tools/remove_directory.py +20 -34
- janito/agent/tools/replace_text_in_file.py +61 -61
- janito/agent/tools/rich_utils.py +6 -6
- janito/agent/tools/run_bash_command.py +66 -120
- janito/agent/tools/search_files.py +21 -47
- janito/agent/tools/tool_base.py +4 -2
- janito/agent/tools/utils.py +31 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/METADATA +6 -6
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/RECORD +26 -24
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/WHEEL +0 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/entry_points.txt +0 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.3.2.dist-info → janito-1.4.0.dist-info}/top_level.txt +0 -0
janito/agent/tools/find_files.py
CHANGED
@@ -1,58 +1,31 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success
|
1
4
|
import os
|
2
5
|
import fnmatch
|
3
|
-
from janito.agent.tool_handler import ToolHandler
|
4
|
-
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
directory: str,
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
print_info(f"🔍 find_files | Dir: {directory} | Pattern: {pattern} | Recursive: {recursive} | Max: {max_results}")
|
24
|
-
# Input validation
|
25
|
-
if not os.path.isdir(directory):
|
26
|
-
print_error(f"❌ Not a directory: {directory}")
|
27
|
-
return ""
|
28
|
-
if not isinstance(max_results, int) or max_results <= 0:
|
29
|
-
print_error(f"❌ Invalid max_results value: {max_results}")
|
30
|
-
return ""
|
31
|
-
matches = []
|
32
|
-
try:
|
33
|
-
if recursive:
|
34
|
-
for root, dirs, files in os.walk(directory):
|
35
|
-
for name in files:
|
36
|
-
if fnmatch.fnmatch(name, pattern):
|
37
|
-
matches.append(os.path.join(root, name))
|
38
|
-
if len(matches) >= max_results:
|
39
|
-
break
|
7
|
+
class FindFilesTool(ToolBase):
|
8
|
+
"""Find files in a directory matching a pattern."""
|
9
|
+
def call(self, directory: str, pattern: str, recursive: bool=False, max_results: int=100) -> str:
|
10
|
+
import os
|
11
|
+
def _display_path(path):
|
12
|
+
import os
|
13
|
+
if os.path.isabs(path):
|
14
|
+
return path
|
15
|
+
return os.path.relpath(path)
|
16
|
+
disp_path = _display_path(directory)
|
17
|
+
rec = "recursively" if recursive else "non-recursively"
|
18
|
+
print_info(f"\U0001F50D Searching '{disp_path}' for pattern '{pattern}' ({rec}, max {max_results})")
|
19
|
+
self.update_progress(f"Searching for files in {directory} matching {pattern}")
|
20
|
+
matches = []
|
21
|
+
for root, dirs, files in os.walk(directory):
|
22
|
+
for filename in fnmatch.filter(files, pattern):
|
23
|
+
matches.append(os.path.join(root, filename))
|
40
24
|
if len(matches) >= max_results:
|
41
25
|
break
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
matches.append(full_path)
|
47
|
-
if len(matches) >= max_results:
|
48
|
-
break
|
49
|
-
except Exception as e:
|
50
|
-
print_error(f"❌ Error during file search: {e}")
|
51
|
-
return ""
|
52
|
-
print_success(f"✅ Found {len(matches)} file(s)")
|
53
|
-
result = f"Total files found: {len(matches)}\n"
|
54
|
-
result += "\n".join(matches)
|
55
|
-
if len(matches) == max_results:
|
56
|
-
result += "\n# WARNING: Results truncated at max_results. There may be more matching files."
|
57
|
-
return result
|
26
|
+
if not recursive:
|
27
|
+
break
|
28
|
+
print_success(f"✅ {len(matches)} found")
|
29
|
+
return "\n".join(matches)
|
58
30
|
|
31
|
+
ToolHandler.register_tool(FindFilesTool, name="find_files")
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
|
4
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
5
|
+
|
6
|
+
class GetFileOutlineTool(ToolBase):
|
7
|
+
"""Get an outline of a file's structure."""
|
8
|
+
def call(self, file_path: str) -> str:
|
9
|
+
print_info(f"📄 Getting outline for: {file_path}")
|
10
|
+
self.update_progress(f"Getting outline for: {file_path}")
|
11
|
+
try:
|
12
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
13
|
+
lines = f.readlines()
|
14
|
+
outline = [line.strip() for line in lines if line.strip()]
|
15
|
+
print_success(f"\u2705 Outline generated for {file_path}")
|
16
|
+
return '\n'.join(outline)
|
17
|
+
except Exception as e:
|
18
|
+
print_error(f"\u274c Error reading file: {e}")
|
19
|
+
return f"Error reading file: {e}"
|
20
|
+
|
21
|
+
ToolHandler.register_tool(GetFileOutlineTool, name="get_file_outline")
|
janito/agent/tools/get_lines.py
CHANGED
@@ -1,58 +1,34 @@
|
|
1
|
-
import
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
2
|
from janito.agent.tool_handler import ToolHandler
|
3
|
-
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
file_path: str,
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
print_success(f"✅ Returned all {total_lines} lines")
|
36
|
-
return f"Total lines in file: {total_lines}\n" + numbered_content
|
37
|
-
# Validate range
|
38
|
-
if from_line is None or to_line is None:
|
39
|
-
print_error(f"❌ Both from_line and to_line must be provided, or neither.")
|
40
|
-
return ""
|
41
|
-
if from_line < 1 or to_line < from_line or (to_line - from_line > 200):
|
42
|
-
print_error(f"❌ Invalid line range: {from_line}-{to_line} for file with {total_lines} lines.")
|
43
|
-
return ""
|
44
|
-
if to_line > total_lines:
|
45
|
-
to_line = total_lines
|
46
|
-
selected = lines[from_line-1:to_line]
|
47
|
-
numbered_content = ''.join(f"{i}: {line}" for i, line in zip(range(from_line, to_line+1), selected))
|
48
|
-
before = lines[:from_line-1]
|
49
|
-
after = lines[to_line:]
|
50
|
-
before_summary = f"... {len(before)} lines before ...\n" if before else ""
|
51
|
-
after_summary = f"... {len(after)} lines after ...\n" if after else ""
|
52
|
-
summary = before_summary + after_summary
|
53
|
-
if from_line == 1 and to_line == total_lines:
|
54
|
-
print_success(f"✅ Returned all {total_lines} lines")
|
55
|
-
else:
|
56
|
-
print_success(f"✅ Returned lines {from_line} to {to_line} of {total_lines}")
|
57
|
-
total_line_info = f"Total lines in file: {total_lines}\n"
|
58
|
-
return total_line_info + summary + numbered_content
|
5
|
+
class GetLinesTool(ToolBase):
|
6
|
+
"""Get specific lines from a file."""
|
7
|
+
def call(self, file_path: str, from_line: int=None, to_line: int=None) -> str:
|
8
|
+
import os
|
9
|
+
def _display_path(path):
|
10
|
+
import os
|
11
|
+
if os.path.isabs(path):
|
12
|
+
return path
|
13
|
+
return os.path.relpath(path)
|
14
|
+
disp_path = _display_path(file_path)
|
15
|
+
if from_line and to_line:
|
16
|
+
count = to_line - from_line + 1
|
17
|
+
print_info(f"📄 Reading {disp_path}:{from_line} ({count} lines)", end="")
|
18
|
+
else:
|
19
|
+
print_info(f"📄 Reading {disp_path} (all lines)", end="")
|
20
|
+
self.update_progress(f"Getting lines {from_line} to {to_line} from {file_path}")
|
21
|
+
try:
|
22
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
23
|
+
lines = f.readlines()
|
24
|
+
selected = lines[(from_line-1 if from_line else 0):(to_line if to_line else None)]
|
25
|
+
if from_line and to_line:
|
26
|
+
print_success(f" ✅ {to_line - from_line + 1} lines read")
|
27
|
+
else:
|
28
|
+
print_success(f" ✅ {len(lines)} lines read")
|
29
|
+
return ''.join(selected)
|
30
|
+
except Exception as e:
|
31
|
+
print_error(f" ❌ Error: {e}")
|
32
|
+
return f"Error reading file: {e}"
|
33
|
+
|
34
|
+
ToolHandler.register_tool(GetLinesTool, name="get_lines")
|
@@ -1,15 +1,17 @@
|
|
1
1
|
import os
|
2
2
|
import pathspec
|
3
|
+
from janito.agent.tools.utils import expand_path
|
3
4
|
|
4
5
|
_spec = None
|
5
6
|
|
6
7
|
|
7
8
|
def load_gitignore_patterns(gitignore_path='.gitignore'):
|
8
9
|
global _spec
|
10
|
+
gitignore_path = expand_path(gitignore_path)
|
9
11
|
if not os.path.exists(gitignore_path):
|
10
12
|
_spec = pathspec.PathSpec.from_lines('gitwildmatch', [])
|
11
13
|
return _spec
|
12
|
-
with open(gitignore_path, 'r') as f:
|
14
|
+
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
13
15
|
lines = f.readlines()
|
14
16
|
_spec = pathspec.PathSpec.from_lines('gitwildmatch', lines)
|
15
17
|
return _spec
|
@@ -17,6 +19,7 @@ def load_gitignore_patterns(gitignore_path='.gitignore'):
|
|
17
19
|
|
18
20
|
def is_ignored(path):
|
19
21
|
global _spec
|
22
|
+
path = expand_path(path)
|
20
23
|
if _spec is None:
|
21
24
|
_spec = load_gitignore_patterns()
|
22
25
|
# Normalize path to be relative and use forward slashes
|
janito/agent/tools/py_compile.py
CHANGED
@@ -1,26 +1,23 @@
|
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
1
2
|
from janito.agent.tool_handler import ToolHandler
|
2
|
-
from janito.agent.tools.rich_utils import print_info
|
3
|
-
import py_compile
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
4
4
|
from typing import Optional
|
5
|
+
import py_compile
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
class PyCompileTool(ToolBase):
|
8
|
+
"""Validate a Python file by compiling it with py_compile."""
|
9
|
+
def call(self, file_path: str, doraise: Optional[bool] = True) -> str:
|
10
|
+
print_info(f"[py_compile] Compiling Python file: {file_path}")
|
11
|
+
self.update_progress(f"Compiling Python file: {file_path}")
|
12
|
+
try:
|
13
|
+
py_compile.compile(file_path, doraise=doraise)
|
14
|
+
print_success(f"[py_compile] Compiled successfully: {file_path}")
|
15
|
+
return f"Compiled successfully: {file_path}"
|
16
|
+
except py_compile.PyCompileError as e:
|
17
|
+
print_error(f"[py_compile] Compile error: {e}")
|
18
|
+
return f"Compile error: {e}"
|
19
|
+
except Exception as e:
|
20
|
+
print_error(f"[py_compile] Error: {e}")
|
21
|
+
return f"Error: {e}"
|
15
22
|
|
16
|
-
|
17
|
-
str: Success message or error details if compilation fails.
|
18
|
-
"""
|
19
|
-
print_info(f"[py_compile_file] Validating Python file: {path}")
|
20
|
-
try:
|
21
|
-
py_compile.compile(path, doraise=doraise)
|
22
|
-
return f"Validation successful: {path} is a valid Python file."
|
23
|
-
except FileNotFoundError:
|
24
|
-
return f"Validation failed: File not found: {path}"
|
25
|
-
except py_compile.PyCompileError as e:
|
26
|
-
return f"Validation failed: {e}"
|
23
|
+
ToolHandler.register_tool(PyCompileTool, name="py_compile_file")
|
@@ -3,7 +3,8 @@ from janito.agent.tools.rich_utils import print_info
|
|
3
3
|
import sys
|
4
4
|
import multiprocessing
|
5
5
|
import io
|
6
|
-
from typing import
|
6
|
+
from typing import Optional
|
7
|
+
from janito.agent.tools.tool_base import ToolBase
|
7
8
|
|
8
9
|
|
9
10
|
def _run_python_code(code: str, result_queue):
|
@@ -20,28 +21,34 @@ def _run_python_code(code: str, result_queue):
|
|
20
21
|
})
|
21
22
|
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
# Converted python_exec function into PythonExecTool subclass
|
25
|
+
class PythonExecTool(ToolBase):
|
25
26
|
"""
|
26
27
|
Execute Python code in a separate process and capture output.
|
27
|
-
|
28
28
|
Args:
|
29
29
|
code (str): The Python code to execute.
|
30
|
-
on_progress (Optional[Callable[[dict], None]]): Optional callback function for streaming progress updates (not used).
|
31
|
-
|
32
30
|
Returns:
|
33
|
-
str:
|
31
|
+
str: Formatted stdout, stderr, and return code.
|
34
32
|
"""
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
33
|
+
def call(self, code: str) -> str:
|
34
|
+
print_info(f"🐍 Executing Python code ...")
|
35
|
+
print_info(code)
|
36
|
+
self.update_progress("Starting Python code execution...")
|
37
|
+
result_queue = multiprocessing.Queue()
|
38
|
+
process = multiprocessing.Process(target=_run_python_code, args=(code, result_queue))
|
39
|
+
process.start()
|
40
|
+
process.join()
|
41
|
+
if not result_queue.empty():
|
42
|
+
result = result_queue.get()
|
43
|
+
else:
|
44
|
+
result = {'stdout': '', 'stderr': 'No result returned from process.', 'returncode': -1}
|
45
|
+
self.update_progress(f"Python code execution completed with return code: {result['returncode']}")
|
46
|
+
if result['returncode'] == 0:
|
47
|
+
from janito.agent.tools.rich_utils import print_success
|
48
|
+
print_success(f"\u2705 Python code executed successfully.")
|
49
|
+
else:
|
50
|
+
from janito.agent.tools.rich_utils import print_error
|
51
|
+
print_error(f"\u274c Python code execution failed with return code {result['returncode']}")
|
52
|
+
return f"stdout:\n{result['stdout']}\nstderr:\n{result['stderr']}\nreturncode: {result['returncode']}"
|
53
|
+
|
54
|
+
ToolHandler.register_tool(PythonExecTool, name="python_exec")
|
@@ -1,38 +1,24 @@
|
|
1
|
-
import
|
2
|
-
import shutil
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
3
2
|
from janito.agent.tool_handler import ToolHandler
|
4
|
-
|
3
|
+
import shutil
|
4
|
+
import os
|
5
5
|
|
6
|
-
|
7
|
-
return not any(os.scandir(path))
|
6
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
class RemoveDirectoryTool(ToolBase):
|
9
|
+
"""Remove a directory. If recursive=False and directory not empty, raises error."""
|
10
|
+
def call(self, directory: str, recursive: bool = False) -> str:
|
11
|
+
print_info(f"🗃️ Removing directory: {directory} (recursive={recursive})")
|
12
|
+
self.update_progress(f"Removing directory: {directory} (recursive={recursive})")
|
13
|
+
try:
|
14
|
+
if recursive:
|
15
|
+
shutil.rmtree(directory)
|
16
|
+
else:
|
17
|
+
os.rmdir(directory)
|
18
|
+
print_success(f"\u2705 Directory removed: {directory}")
|
19
|
+
return f"Directory removed: {directory}"
|
20
|
+
except Exception as e:
|
21
|
+
print_error(f"\u274c Error removing directory: {e}")
|
22
|
+
return f"Error removing directory: {e}"
|
13
23
|
|
14
|
-
|
15
|
-
path (str): Path to the directory to remove.
|
16
|
-
recursive (bool): Whether to remove non-empty directories recursively. Default is False.
|
17
|
-
Returns:
|
18
|
-
str: Result message.
|
19
|
-
"""
|
20
|
-
if not os.path.exists(path):
|
21
|
-
print_error(f"❌ Directory '{path}' does not exist.")
|
22
|
-
return f"❌ Directory '{path}' does not exist."
|
23
|
-
if not os.path.isdir(path):
|
24
|
-
print_error(f"❌ Path '{path}' is not a directory.")
|
25
|
-
return f"❌ Path '{path}' is not a directory."
|
26
|
-
if recursive:
|
27
|
-
print_info(f"🗑️ Recursively removing directory: '{format_path(path)}' ... ")
|
28
|
-
shutil.rmtree(path)
|
29
|
-
print_success("✅ Success")
|
30
|
-
return f"✅ Successfully removed directory and all contents at '{path}'."
|
31
|
-
else:
|
32
|
-
if not _is_dir_empty(path):
|
33
|
-
print_error(f"❌ Directory '{path}' is not empty. Use recursive=True to remove non-empty directories.")
|
34
|
-
return f"❌ Directory '{path}' is not empty. Use recursive=True to remove non-empty directories."
|
35
|
-
print_info(f"🗑️ Removing empty directory: '{format_path(path)}' ... ")
|
36
|
-
os.rmdir(path)
|
37
|
-
print_success("✅ Success")
|
38
|
-
return f"✅ Successfully removed empty directory at '{path}'."
|
24
|
+
ToolHandler.register_tool(RemoveDirectoryTool, name="remove_directory")
|
@@ -1,67 +1,67 @@
|
|
1
|
-
import
|
1
|
+
from janito.agent.tools.tool_base import ToolBase
|
2
2
|
from janito.agent.tool_handler import ToolHandler
|
3
3
|
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
"""
|
5
|
+
class ReplaceTextInFileTool(ToolBase):
|
6
|
+
"""Replace exact occurrences of a given text in a file.
|
8
7
|
|
9
|
-
|
8
|
+
NOTE: Indentation (leading whitespace) must be included in both search_text and replacement_text. This tool does not automatically adjust or infer indentation; matches are exact, including whitespace.
|
9
|
+
"""
|
10
|
+
def call(self, file_path: str, search_text: str, replacement_text: str, replace_all: bool = False) -> str:
|
11
|
+
"""
|
12
|
+
Replace exact occurrences of a given text in a file.
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
print_error(f"❌ Permission denied: {file_path}")
|
31
|
-
return f"❌ Error: Permission denied: {file_path}"
|
32
|
-
except Exception as e:
|
33
|
-
print_error(f"❌ Error reading file: {e}")
|
34
|
-
return f"❌ Error reading file: {e}"
|
14
|
+
Args:
|
15
|
+
file_path (str): Path to the file.
|
16
|
+
search_text (str): Text to search for. Must include indentation (leading whitespace) if present in the file.
|
17
|
+
replacement_text (str): Replacement text. Must include desired indentation (leading whitespace).
|
18
|
+
replace_all (bool): If True, replace all occurrences; otherwise, only the first occurrence.
|
19
|
+
Returns:
|
20
|
+
str: Status message.
|
21
|
+
"""
|
22
|
+
import os
|
23
|
+
filename = os.path.basename(file_path)
|
24
|
+
action = "all occurrences" if replace_all else "first occurrence"
|
25
|
+
# Show only concise info (lengths, not full content)
|
26
|
+
search_preview = (search_text[:20] + '...') if len(search_text) > 20 else search_text
|
27
|
+
replace_preview = (replacement_text[:20] + '...') if len(replacement_text) > 20 else replacement_text
|
28
|
+
print_info(f"\U0001F4DD Replacing in {filename}: '{search_preview}' '{replace_preview}' ({action})", end="")
|
29
|
+
self.update_progress(f"Replacing text in {file_path}")
|
30
|
+
try:
|
31
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
32
|
+
content = f.read()
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
34
|
+
if replace_all:
|
35
|
+
replaced_count = content.count(search_text)
|
36
|
+
new_content = content.replace(search_text, replacement_text)
|
37
|
+
else:
|
38
|
+
occurrences = content.count(search_text)
|
39
|
+
if occurrences > 1:
|
40
|
+
print_error(f" ❌ Error: Search text is not unique ({occurrences} occurrences found). Provide more detailed context.")
|
41
|
+
return f"Error: Search text is not unique ({occurrences} occurrences found) in {file_path}. Provide more detailed context for unique replacement."
|
42
|
+
replaced_count = 1 if occurrences == 1 else 0
|
43
|
+
new_content = content.replace(search_text, replacement_text, 1)
|
44
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
45
|
+
f.write(new_content)
|
46
|
+
warning = ''
|
47
|
+
if replaced_count == 0:
|
48
|
+
warning = f" [Warning: Search text not found in file]"
|
49
|
+
print_error(warning)
|
50
|
+
print_success(f" ✅ {replaced_count} replaced{warning}")
|
51
|
+
# Indentation check for agent warning
|
52
|
+
def leading_ws(line):
|
53
|
+
import re
|
54
|
+
m = re.match(r"^\s*", line)
|
55
|
+
return m.group(0) if m else ''
|
56
|
+
search_indent = leading_ws(search_text.splitlines()[0]) if search_text.splitlines() else ''
|
57
|
+
replace_indent = leading_ws(replacement_text.splitlines()[0]) if replacement_text.splitlines() else ''
|
58
|
+
indent_warning = ''
|
59
|
+
if search_indent != replace_indent:
|
60
|
+
indent_warning = f" [Warning: Indentation mismatch between search and replacement text: '{search_indent}' vs '{replace_indent}']"
|
61
|
+
return f"Text replaced in {file_path}{warning}{indent_warning}"
|
62
|
+
|
63
|
+
except Exception as e:
|
64
|
+
print_error(f" ❌ Error: {e}")
|
65
|
+
return f"Error replacing text: {e}"
|
66
|
+
|
67
|
+
ToolHandler.register_tool(ReplaceTextInFileTool, name="replace_text_in_file")
|
janito/agent/tools/rich_utils.py
CHANGED
@@ -3,14 +3,14 @@ from rich.text import Text
|
|
3
3
|
|
4
4
|
console = Console()
|
5
5
|
|
6
|
-
def print_info(message: str):
|
7
|
-
console.print(message, style="cyan")
|
6
|
+
def print_info(message: str, end="\n"):
|
7
|
+
console.print(message, style="cyan", end=end)
|
8
8
|
|
9
|
-
def print_success(message: str):
|
10
|
-
console.print(message, style="bold green")
|
9
|
+
def print_success(message: str, end="\n"):
|
10
|
+
console.print(message, style="bold green", end=end)
|
11
11
|
|
12
|
-
def print_error(message: str):
|
13
|
-
console.print(message, style="bold red")
|
12
|
+
def print_error(message: str, end="\n"):
|
13
|
+
console.print(message, style="bold red", end=end)
|
14
14
|
|
15
15
|
def print_warning(message: str):
|
16
16
|
console.print(message, style="yellow")
|