janito 1.2.1__py3-none-any.whl → 1.3.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/agent.py +23 -10
- janito/agent/config.py +37 -9
- janito/agent/conversation.py +8 -0
- janito/agent/runtime_config.py +1 -0
- janito/agent/tool_handler.py +154 -52
- janito/agent/tools/__init__.py +8 -5
- janito/agent/tools/ask_user.py +2 -1
- janito/agent/tools/fetch_url.py +27 -35
- janito/agent/tools/file_ops.py +72 -67
- janito/agent/tools/find_files.py +47 -26
- janito/agent/tools/get_lines.py +58 -0
- janito/agent/tools/py_compile.py +26 -0
- janito/agent/tools/python_exec.py +47 -0
- janito/agent/tools/remove_directory.py +38 -0
- janito/agent/tools/replace_text_in_file.py +67 -0
- janito/agent/tools/run_bash_command.py +134 -0
- janito/agent/tools/search_files.py +52 -0
- janito/cli/_print_config.py +1 -1
- janito/cli/arg_parser.py +6 -1
- janito/cli/config_commands.py +56 -8
- janito/cli/runner.py +21 -9
- janito/cli_chat_shell/chat_loop.py +5 -3
- janito/cli_chat_shell/commands.py +34 -37
- janito/cli_chat_shell/config_shell.py +1 -1
- janito/cli_chat_shell/load_prompt.py +1 -1
- janito/cli_chat_shell/session_manager.py +11 -15
- janito/cli_chat_shell/ui.py +17 -8
- janito/render_prompt.py +3 -1
- janito/web/app.py +1 -1
- janito-1.3.0.dist-info/METADATA +142 -0
- janito-1.3.0.dist-info/RECORD +51 -0
- janito/agent/tools/bash_exec.py +0 -58
- janito/agent/tools/file_str_replace.py +0 -48
- janito/agent/tools/search_text.py +0 -41
- janito/agent/tools/view_file.py +0 -34
- janito/templates/system_instructions.j2 +0 -38
- janito-1.2.1.dist-info/METADATA +0 -85
- janito-1.2.1.dist-info/RECORD +0 -49
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/WHEEL +0 -0
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/entry_points.txt +0 -0
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.2.1.dist-info → janito-1.3.0.dist-info}/top_level.txt +0 -0
janito/agent/tools/file_ops.py
CHANGED
@@ -1,67 +1,72 @@
|
|
1
|
-
import os
|
2
|
-
import shutil
|
3
|
-
from janito.agent.tool_handler import ToolHandler
|
4
|
-
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
|
5
|
-
|
6
|
-
@ToolHandler.register_tool
|
7
|
-
def create_file(path: str, content: str, overwrite: bool = False) -> 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
|
-
|
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
|
-
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
from janito.agent.tool_handler import ToolHandler
|
4
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
|
5
|
+
|
6
|
+
@ToolHandler.register_tool
|
7
|
+
def create_file(path: str, content: str, overwrite: bool = False) -> str:
|
8
|
+
"""
|
9
|
+
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
|
+
"""
|
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:
|
36
|
+
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}'."
|
40
|
+
|
41
|
+
|
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}'."
|
48
|
+
|
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}'."
|
66
|
+
|
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."
|
janito/agent/tools/find_files.py
CHANGED
@@ -1,37 +1,58 @@
|
|
1
1
|
import os
|
2
2
|
import fnmatch
|
3
3
|
from janito.agent.tool_handler import ToolHandler
|
4
|
-
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
5
|
-
from janito.agent.tools.gitignore_utils import load_gitignore_patterns, filter_ignored
|
4
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
6
5
|
|
7
6
|
@ToolHandler.register_tool
|
8
|
-
def find_files(
|
7
|
+
def find_files(
|
8
|
+
directory: str,
|
9
|
+
pattern: str,
|
10
|
+
recursive: bool = False,
|
11
|
+
max_results: int = 100
|
12
|
+
) -> str:
|
9
13
|
"""
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
Find files in a directory matching a pattern.
|
15
|
+
Args:
|
16
|
+
directory (str): The directory to search in.
|
17
|
+
pattern (str): The filename pattern to match (e.g., '*.txt').
|
18
|
+
recursive (bool): Whether to search subdirectories.
|
19
|
+
max_results (int): Maximum number of results to return.
|
20
|
+
Returns:
|
21
|
+
str: Newline-separated list of matching file paths, with summary and warnings if truncated.
|
14
22
|
"""
|
15
|
-
print_info(f"🔍
|
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 ""
|
23
31
|
matches = []
|
24
|
-
ignore_patterns = load_gitignore_patterns()
|
25
32
|
try:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
40
|
+
if len(matches) >= max_results:
|
41
|
+
break
|
33
42
|
else:
|
34
|
-
|
43
|
+
for name in os.listdir(directory):
|
44
|
+
full_path = os.path.join(directory, name)
|
45
|
+
if os.path.isfile(full_path) and fnmatch.fnmatch(name, pattern):
|
46
|
+
matches.append(full_path)
|
47
|
+
if len(matches) >= max_results:
|
48
|
+
break
|
35
49
|
except Exception as e:
|
36
|
-
print_error(f"❌ Error: {e}")
|
37
|
-
return
|
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
|
58
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import os
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path, format_number
|
4
|
+
|
5
|
+
@ToolHandler.register_tool
|
6
|
+
def get_lines(
|
7
|
+
file_path: str,
|
8
|
+
from_line: int = None,
|
9
|
+
to_line: int = None
|
10
|
+
) -> str:
|
11
|
+
"""
|
12
|
+
Get lines from a file, optionally with a summary of lines outside the viewed range.
|
13
|
+
Always returns the total number of lines in the file at the top of the output.
|
14
|
+
|
15
|
+
Parameters:
|
16
|
+
- file_path (string): Path to the file.
|
17
|
+
- from_line (integer, optional): First line to view (1-indexed). If omitted with to_line, returns all lines.
|
18
|
+
- to_line (integer, optional): Last line to view (inclusive, 1-indexed, and cannot be more than 200 lines from from_line).
|
19
|
+
|
20
|
+
If both from_line and to_line are omitted, returns all lines in the file.
|
21
|
+
It is recommended to request at least 100 lines or the full file for more efficient context building.
|
22
|
+
"""
|
23
|
+
if from_line is None and to_line is None:
|
24
|
+
print_info(f"📂 get_lines | Path: {format_path(file_path)} | All lines requested")
|
25
|
+
else:
|
26
|
+
print_info(f"📂 get_lines | Path: {format_path(file_path)} | Lines ({from_line}-{to_line})")
|
27
|
+
if not os.path.isfile(file_path):
|
28
|
+
print_info(f"ℹ️ File not found: {file_path}")
|
29
|
+
return ""
|
30
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
31
|
+
lines = f.readlines()
|
32
|
+
total_lines = len(lines)
|
33
|
+
if from_line is None and to_line is None:
|
34
|
+
numbered_content = ''.join(f"{i+1}: {line}" for i, line in enumerate(lines))
|
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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from janito.agent.tool_handler import ToolHandler
|
2
|
+
from janito.agent.tools.rich_utils import print_info
|
3
|
+
import py_compile
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
@ToolHandler.register_tool
|
7
|
+
def py_compile_file(path: str, doraise: Optional[bool] = True) -> str:
|
8
|
+
"""
|
9
|
+
Validate a Python file by compiling it with py_compile.
|
10
|
+
This tool should be used to validate Python files after changes.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
path (str): Path to the Python file to validate.
|
14
|
+
doraise (Optional[bool]): If True, raise exceptions on compilation errors. Default is True.
|
15
|
+
|
16
|
+
Returns:
|
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}"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from janito.agent.tool_handler import ToolHandler
|
2
|
+
from janito.agent.tools.rich_utils import print_info
|
3
|
+
import sys
|
4
|
+
import multiprocessing
|
5
|
+
import io
|
6
|
+
from typing import Callable, Optional
|
7
|
+
|
8
|
+
|
9
|
+
def _run_python_code(code: str, result_queue):
|
10
|
+
import traceback
|
11
|
+
import contextlib
|
12
|
+
stdout = io.StringIO()
|
13
|
+
stderr = io.StringIO()
|
14
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
15
|
+
exec(code, {'__name__': '__main__'})
|
16
|
+
result_queue.put({
|
17
|
+
'stdout': stdout.getvalue(),
|
18
|
+
'stderr': stderr.getvalue(),
|
19
|
+
'returncode': 0
|
20
|
+
})
|
21
|
+
|
22
|
+
|
23
|
+
@ToolHandler.register_tool
|
24
|
+
def python_exec(code: str, on_progress: Optional[Callable[[dict], None]] = None) -> str:
|
25
|
+
"""
|
26
|
+
Execute Python code in a separate process and capture output.
|
27
|
+
|
28
|
+
Args:
|
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
|
+
Returns:
|
33
|
+
str: A formatted message string containing stdout, stderr, and return code.
|
34
|
+
"""
|
35
|
+
print_info(f"[python_exec] Executing Python code:")
|
36
|
+
print_info(code)
|
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
|
+
print_info(f"[python_exec] Execution completed.")
|
46
|
+
print_info(f"[python_exec] Return code: {result['returncode']}")
|
47
|
+
return f"stdout:\n{result['stdout']}\nstderr:\n{result['stderr']}\nreturncode: {result['returncode']}"
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
from janito.agent.tool_handler import ToolHandler
|
4
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
|
5
|
+
|
6
|
+
def _is_dir_empty(path):
|
7
|
+
return not any(os.scandir(path))
|
8
|
+
|
9
|
+
@ToolHandler.register_tool
|
10
|
+
def remove_directory(path: str, recursive: bool = False) -> str:
|
11
|
+
"""
|
12
|
+
Remove a directory. If recursive is False and the directory is not empty, return an error.
|
13
|
+
|
14
|
+
Args:
|
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}'."
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import os
|
2
|
+
from janito.agent.tool_handler import ToolHandler
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
|
4
|
+
|
5
|
+
@ToolHandler.register_tool
|
6
|
+
def replace_text_in_file(file_path: str, search_text: str, replacement_text: str, replace_all: bool = False) -> str:
|
7
|
+
"""
|
8
|
+
|
9
|
+
Replace exact occurrences of a given text in a file. The match must be exact, including whitespace and indentation, to avoid breaking file syntax or formatting.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
file_path (str): Path to the plain text file.
|
13
|
+
search_text (str): Text to search for (exact match).
|
14
|
+
replacement_text (str): Text to replace search_text with.
|
15
|
+
replace_all (bool): Whether to replace all occurrences or just the first. Default is False.
|
16
|
+
Returns:
|
17
|
+
str: Result message.
|
18
|
+
"""
|
19
|
+
search_preview = (search_text[:15] + '...') if len(search_text) > 15 else search_text
|
20
|
+
replace_preview = (replacement_text[:15] + '...') if len(replacement_text) > 15 else replacement_text
|
21
|
+
replace_all_msg = f" | Replace all: True" if replace_all else ""
|
22
|
+
print_info(f"📝 replace_text_in_file | Path: {format_path(file_path)} | Search: '{search_preview}' | Replacement: '{replace_preview}'{replace_all_msg}")
|
23
|
+
if not os.path.isfile(file_path):
|
24
|
+
print_error(f"❌ File not found: {file_path}")
|
25
|
+
return f"❌ Error: File not found: {file_path}"
|
26
|
+
try:
|
27
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
28
|
+
content = f.read()
|
29
|
+
except PermissionError:
|
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}"
|
35
|
+
|
36
|
+
count = content.count(search_text)
|
37
|
+
if count == 0:
|
38
|
+
print_info(f"ℹ️ Search text not found in file.")
|
39
|
+
return f"ℹ️ No occurrences of search text found in '{file_path}'."
|
40
|
+
if replace_all:
|
41
|
+
new_content = content.replace(search_text, replacement_text)
|
42
|
+
replaced_count = count
|
43
|
+
else:
|
44
|
+
if count > 1:
|
45
|
+
# Find line numbers where search_text appears
|
46
|
+
lines = content.splitlines()
|
47
|
+
found_lines = [i+1 for i, line in enumerate(lines) if search_text in line]
|
48
|
+
preview = search_text[:40] + ('...' if len(search_text) > 40 else '')
|
49
|
+
print_error(f"❌ Search text found multiple times ({count}). Please provide a more exact match or set replace_all=True.")
|
50
|
+
return (
|
51
|
+
f"❌ Error: Search text found {count} times in '{file_path}'. "
|
52
|
+
f"Preview: '{preview}'. Found at lines: {found_lines}. "
|
53
|
+
f"Please provide a more exact match."
|
54
|
+
)
|
55
|
+
new_content = content.replace(search_text, replacement_text, 1)
|
56
|
+
replaced_count = 1 if count == 1 else 0
|
57
|
+
try:
|
58
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
59
|
+
f.write(new_content)
|
60
|
+
except PermissionError:
|
61
|
+
print_error(f"❌ Permission denied when writing: {file_path}")
|
62
|
+
return f"❌ Error: Permission denied when writing: {file_path}"
|
63
|
+
except Exception as e:
|
64
|
+
print_error(f"❌ Error writing file: {e}")
|
65
|
+
return f"❌ Error writing file: {e}"
|
66
|
+
print_success(f"✅ Replaced {replaced_count} occurrence(s) of search text in '{file_path}'.")
|
67
|
+
return f"✅ Replaced {replaced_count} occurrence(s) of search text in '{file_path}'."
|
@@ -0,0 +1,134 @@
|
|
1
|
+
from janito.agent.tool_handler import ToolHandler
|
2
|
+
from janito.agent.runtime_config import runtime_config
|
3
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error
|
4
|
+
import subprocess
|
5
|
+
import multiprocessing
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
import tempfile
|
9
|
+
import os
|
10
|
+
|
11
|
+
def _run_bash_command(command: str, result_queue: 'multiprocessing.Queue', trust: bool = False):
|
12
|
+
import subprocess
|
13
|
+
with tempfile.NamedTemporaryFile(delete=False, mode='w+', encoding='utf-8', suffix='.stdout') as stdout_file, \
|
14
|
+
tempfile.NamedTemporaryFile(delete=False, mode='w+', encoding='utf-8', suffix='.stderr') as stderr_file:
|
15
|
+
process = subprocess.Popen(
|
16
|
+
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace'
|
17
|
+
)
|
18
|
+
while True:
|
19
|
+
stdout_line = process.stdout.readline() if process.stdout else ''
|
20
|
+
stderr_line = process.stderr.readline() if process.stderr else ''
|
21
|
+
if stdout_line:
|
22
|
+
if not trust:
|
23
|
+
print(stdout_line, end='')
|
24
|
+
stdout_file.write(stdout_line)
|
25
|
+
stdout_file.flush()
|
26
|
+
if stderr_line:
|
27
|
+
if not trust:
|
28
|
+
print(stderr_line, end='')
|
29
|
+
stderr_file.write(stderr_line)
|
30
|
+
stderr_file.flush()
|
31
|
+
if not stdout_line and not stderr_line and process.poll() is not None:
|
32
|
+
break
|
33
|
+
# Capture any remaining output after process ends
|
34
|
+
if process.stdout:
|
35
|
+
for line in process.stdout:
|
36
|
+
if not trust:
|
37
|
+
print(line, end='')
|
38
|
+
stdout_file.write(line)
|
39
|
+
if process.stderr:
|
40
|
+
for line in process.stderr:
|
41
|
+
if not trust:
|
42
|
+
print(line, end='')
|
43
|
+
stderr_file.write(line)
|
44
|
+
stdout_file_path = stdout_file.name
|
45
|
+
stderr_file_path = stderr_file.name
|
46
|
+
result_queue.put({
|
47
|
+
'stdout_file': stdout_file_path,
|
48
|
+
'stderr_file': stderr_file_path,
|
49
|
+
'returncode': process.returncode
|
50
|
+
})
|
51
|
+
|
52
|
+
|
53
|
+
@ToolHandler.register_tool
|
54
|
+
def run_bash_command(command: str, timeout: int = 60, require_confirmation: bool = False) -> str:
|
55
|
+
trust = runtime_config.get('trust', False)
|
56
|
+
"""
|
57
|
+
Execute a non-interactive bash command and print output live.
|
58
|
+
|
59
|
+
If require_confirmation is True, the user will be prompted to confirm execution before running the command.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
command (str): The Bash command to execute.
|
63
|
+
timeout (int): Maximum number of seconds to allow the command to run. Default is 60.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
str: A formatted message string containing stdout, stderr, and return code.
|
67
|
+
"""
|
68
|
+
print_info(f"[run_bash_command] Running: {command}")
|
69
|
+
if require_confirmation:
|
70
|
+
# Prompt the user for confirmation directly
|
71
|
+
resp = input(f"Are you sure you want to run this command?\n\n{command}\n\nType 'yes' to confirm: ")
|
72
|
+
if resp.strip().lower() != 'yes':
|
73
|
+
print_error("❌ Command not confirmed by user.")
|
74
|
+
return "❌ Command not confirmed by user."
|
75
|
+
print_info(f"🐛 Running bash command: [bold]{command}[/bold] (timeout: {timeout}s)")
|
76
|
+
result_queue = multiprocessing.Queue()
|
77
|
+
process = multiprocessing.Process(target=_run_bash_command, args=(command, result_queue, trust))
|
78
|
+
process.start()
|
79
|
+
process.join(timeout)
|
80
|
+
if process.is_alive():
|
81
|
+
process.terminate()
|
82
|
+
process.join()
|
83
|
+
result = {'stdout_file': '', 'stderr_file': '', 'error': f'Process timed out after {timeout} seconds.', 'returncode': -1}
|
84
|
+
elif not result_queue.empty():
|
85
|
+
result = result_queue.get()
|
86
|
+
else:
|
87
|
+
result = {'stdout_file': '', 'stderr_file': '', 'error': 'No result returned from process.', 'returncode': -1}
|
88
|
+
if trust:
|
89
|
+
stdout_lines = 0
|
90
|
+
stderr_lines = 0
|
91
|
+
try:
|
92
|
+
with open(result['stdout_file'], 'r', encoding='utf-8') as f:
|
93
|
+
stdout_lines = sum(1 for _ in f)
|
94
|
+
except Exception:
|
95
|
+
pass
|
96
|
+
try:
|
97
|
+
with open(result['stderr_file'], 'r', encoding='utf-8') as f:
|
98
|
+
stderr_lines = sum(1 for _ in f)
|
99
|
+
except Exception:
|
100
|
+
pass
|
101
|
+
print_success(f"✅ Success (trust mode)\nstdout: {result['stdout_file']} (lines: {stdout_lines})\nstderr: {result['stderr_file']} (lines: {stderr_lines})")
|
102
|
+
return (
|
103
|
+
f"✅ Bash command executed in trust mode. Output is stored at:\n"
|
104
|
+
f"stdout: {result['stdout_file']} (lines: {stdout_lines})\n"
|
105
|
+
f"stderr: {result['stderr_file']} (lines: {stderr_lines})\n"
|
106
|
+
f"returncode: {result['returncode']}\n"
|
107
|
+
"To examine the output, use the file-related tools such as get_lines or search_files on the above files."
|
108
|
+
)
|
109
|
+
print_info("🐛 Bash command execution completed.")
|
110
|
+
print_info(f"Return code: {result['returncode']}")
|
111
|
+
if result.get('error'):
|
112
|
+
print_error(f"Error: {result['error']}")
|
113
|
+
return f"❌ Error: {result['error']}\nreturncode: {result['returncode']}"
|
114
|
+
stdout_lines = 0
|
115
|
+
stderr_lines = 0
|
116
|
+
try:
|
117
|
+
with open(result['stdout_file'], 'r', encoding='utf-8') as f:
|
118
|
+
stdout_lines = sum(1 for _ in f)
|
119
|
+
except Exception:
|
120
|
+
pass
|
121
|
+
try:
|
122
|
+
with open(result['stderr_file'], 'r', encoding='utf-8') as f:
|
123
|
+
stderr_lines = sum(1 for _ in f)
|
124
|
+
except Exception:
|
125
|
+
pass
|
126
|
+
print_success(f"✅ Success\nstdout saved to: {result['stdout_file']} (lines: {stdout_lines})\nstderr saved to: {result['stderr_file']} (lines: {stderr_lines})")
|
127
|
+
return (
|
128
|
+
f"✅ Bash command executed.\n"
|
129
|
+
f"stdout saved to: {result['stdout_file']} (lines: {stdout_lines})\n"
|
130
|
+
f"stderr saved to: {result['stderr_file']} (lines: {stderr_lines})\n"
|
131
|
+
f"returncode: {result['returncode']}\n"
|
132
|
+
"\nTo examine the output, use the file-related tools such as get_lines or search_files on the above files."
|
133
|
+
)
|
134
|
+
|