janito 1.2.0__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 +27 -10
- janito/agent/config.py +37 -9
- janito/agent/config_utils.py +9 -0
- janito/agent/conversation.py +19 -3
- janito/agent/runtime_config.py +1 -0
- janito/agent/tool_handler.py +154 -52
- janito/agent/tools/__init__.py +9 -8
- janito/agent/tools/ask_user.py +2 -1
- janito/agent/tools/fetch_url.py +27 -35
- janito/agent/tools/file_ops.py +72 -0
- 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 +12 -12
- 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 +76 -19
- 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/create_directory.py +0 -19
- janito/agent/tools/create_file.py +0 -43
- janito/agent/tools/file_str_replace.py +0 -48
- janito/agent/tools/move_file.py +0 -37
- janito/agent/tools/remove_file.py +0 -19
- 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.0.dist-info/METADATA +0 -85
- janito-1.2.0.dist-info/RECORD +0 -51
- {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/WHEEL +0 -0
- {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/entry_points.txt +0 -0
- {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/top_level.txt +0 -0
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
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
import fnmatch
|
4
|
+
from janito.agent.tool_handler import ToolHandler
|
5
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path, format_number
|
6
|
+
from janito.agent.tools.gitignore_utils import load_gitignore_patterns, filter_ignored
|
7
|
+
|
8
|
+
@ToolHandler.register_tool
|
9
|
+
def search_files(
|
10
|
+
directory: str,
|
11
|
+
pattern: str
|
12
|
+
) -> str:
|
13
|
+
"""
|
14
|
+
Search for a text pattern in all files within a directory and return matching lines and their content.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
directory (str): The directory to search in.
|
18
|
+
pattern (str): The text pattern to search for.
|
19
|
+
Returns:
|
20
|
+
str: Each match as 'filepath:lineno:linecontent', one per line.
|
21
|
+
"""
|
22
|
+
print_info(f"🔎 search_files | Path: {directory} | pattern: '{pattern}'")
|
23
|
+
results = []
|
24
|
+
ignore_patterns = load_gitignore_patterns()
|
25
|
+
try:
|
26
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
27
|
+
except re.error:
|
28
|
+
regex = None
|
29
|
+
|
30
|
+
files_to_search = []
|
31
|
+
if os.path.isfile(directory):
|
32
|
+
files_to_search = [directory]
|
33
|
+
else:
|
34
|
+
for root, dirs, files in os.walk(directory):
|
35
|
+
dirs, files = filter_ignored(root, dirs, files, ignore_patterns)
|
36
|
+
for file in files:
|
37
|
+
filepath = os.path.join(root, file)
|
38
|
+
files_to_search.append(filepath)
|
39
|
+
|
40
|
+
for filepath in files_to_search:
|
41
|
+
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
42
|
+
for lineno, line in enumerate(f, start=1):
|
43
|
+
if regex:
|
44
|
+
if regex.search(line):
|
45
|
+
results.append(f"{filepath}:{lineno}:{line.rstrip()}")
|
46
|
+
else:
|
47
|
+
if pattern.lower() in line.lower():
|
48
|
+
results.append(f"{filepath}:{lineno}:{line.rstrip()}")
|
49
|
+
|
50
|
+
print_success(f"✅ Found {format_number(len(results))} matches")
|
51
|
+
return "\n".join(results)
|
52
|
+
|
janito/cli/_print_config.py
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
import os
|
2
|
-
from
|
2
|
+
from janito.agent.tools.rich_utils import print_info, print_success, print_error, print_warning, print_magenta
|
3
3
|
from ._utils import home_shorten
|
4
4
|
|
5
5
|
def print_config_items(items, color_label=None):
|
6
6
|
if not items:
|
7
7
|
return
|
8
8
|
if color_label:
|
9
|
-
|
9
|
+
print_info(color_label)
|
10
10
|
home = os.path.expanduser("~")
|
11
11
|
for key, value in items.items():
|
12
12
|
if key == "system_prompt" and isinstance(value, str):
|
13
13
|
if value.startswith(home):
|
14
14
|
print(f"{key} = {home_shorten(value)}")
|
15
15
|
else:
|
16
|
-
|
16
|
+
print_info(f"{key} = {value}")
|
17
17
|
else:
|
18
|
-
|
19
|
-
|
18
|
+
print_info(f"{key} = {value}")
|
19
|
+
print_info("")
|
20
20
|
|
21
21
|
def print_full_config(local_config, global_config, unified_config, config_defaults, console=None):
|
22
22
|
"""
|
@@ -28,9 +28,9 @@ def print_full_config(local_config, global_config, unified_config, config_defaul
|
|
28
28
|
local_keys = set(local_config.all().keys())
|
29
29
|
global_keys = set(global_config.all().keys())
|
30
30
|
all_keys = set(config_defaults.keys()) | global_keys | local_keys
|
31
|
-
out =
|
31
|
+
out = print_info if console is None else console.print
|
32
32
|
if not (local_keys or global_keys):
|
33
|
-
|
33
|
+
print_warning("No configuration found.")
|
34
34
|
else:
|
35
35
|
for key in sorted(local_keys):
|
36
36
|
if key == "api_key":
|
@@ -57,12 +57,12 @@ def print_full_config(local_config, global_config, unified_config, config_defaul
|
|
57
57
|
shown_keys = set(local_items.keys()) | set(global_items.keys())
|
58
58
|
default_items = {k: v for k, v in config_defaults.items() if k not in shown_keys and k != 'api_key'}
|
59
59
|
if default_items:
|
60
|
-
|
60
|
+
print_magenta("[green]🟢 Defaults (not set in config files)[/green]")
|
61
61
|
from pathlib import Path
|
62
|
-
template_path = Path(__file__).parent
|
62
|
+
template_path = Path(__file__).parent / "agent" / "templates" / "system_instructions.j2"
|
63
63
|
for key, value in default_items.items():
|
64
64
|
if key == "system_prompt" and value is None:
|
65
|
-
|
65
|
+
print_info(f"{key} = (default template path: {home_shorten(str(template_path))})")
|
66
66
|
else:
|
67
|
-
|
68
|
-
|
67
|
+
print_info(f"{key} = {value}")
|
68
|
+
print_info("")
|
janito/cli/arg_parser.py
CHANGED
@@ -4,8 +4,11 @@ import argparse
|
|
4
4
|
def create_parser():
|
5
5
|
parser = argparse.ArgumentParser(description="OpenRouter API call using OpenAI Python SDK")
|
6
6
|
parser.add_argument("prompt", type=str, nargs="?", help="Prompt to send to the model")
|
7
|
+
|
7
8
|
parser.add_argument("--max-tokens", type=int, default=None, help="Maximum tokens for model response (overrides config, default: 200000)")
|
9
|
+
parser.add_argument("--max-tools", type=int, default=None, help="Maximum number of tool calls allowed within a chat session (default: unlimited)")
|
8
10
|
parser.add_argument("--model", type=str, default=None, help="Model name to use for this session (overrides config, does not persist)")
|
11
|
+
parser.add_argument("--max-rounds", type=int, default=None, help="Maximum number of agent rounds per prompt (overrides config, default: 50)")
|
9
12
|
|
10
13
|
# Mutually exclusive group for system prompt options
|
11
14
|
group = parser.add_mutually_exclusive_group()
|
@@ -19,9 +22,10 @@ def create_parser():
|
|
19
22
|
parser.add_argument("--verbose-response", action="store_true", help="Pretty print the full response object")
|
20
23
|
parser.add_argument("--show-system", action="store_true", help="Show model, parameters, system prompt, and tool definitions, then exit")
|
21
24
|
parser.add_argument("--verbose-tools", action="store_true", help="Print tool call parameters and results")
|
22
|
-
parser.add_argument("--
|
25
|
+
parser.add_argument("-n", "--no-tools", action="store_true", default=False, help="Disable tool use (default: enabled)")
|
23
26
|
parser.add_argument("--set-local-config", type=str, default=None, help='Set a local config key-value pair, format "key=val"')
|
24
27
|
parser.add_argument("--set-global-config", type=str, default=None, help='Set a global config key-value pair, format "key=val"')
|
28
|
+
parser.add_argument("--run-config", type=str, action='append', default=None, help='Set a runtime (in-memory only) config key-value pair, format "key=val". Can be repeated.')
|
25
29
|
parser.add_argument("--show-config", action="store_true", help="Show effective configuration and exit")
|
26
30
|
parser.add_argument("--set-api-key", type=str, default=None, help="Set and save the API key globally")
|
27
31
|
parser.add_argument("--version", action="store_true", help="Show program's version number and exit")
|
@@ -30,4 +34,5 @@ def create_parser():
|
|
30
34
|
parser.add_argument("--web", action="store_true", help="Launch the Janito web server instead of CLI")
|
31
35
|
parser.add_argument("--config-reset-local", action="store_true", help="Remove the local config file (~/.janito/config.json)")
|
32
36
|
parser.add_argument("--config-reset-global", action="store_true", help="Remove the global config file (~/.janito/config.json)")
|
37
|
+
parser.add_argument("--trust", action="store_true", help="Enable trust mode: suppresses run_bash_command output, only shows output file locations.")
|
33
38
|
return parser
|