janito 0.14.0__py3-none-any.whl → 1.0.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 -5
- janito/__main__.py +3 -5
- janito/agent/__init__.py +1 -0
- janito/agent/agent.py +96 -0
- janito/agent/config.py +113 -0
- janito/agent/config_defaults.py +10 -0
- janito/agent/conversation.py +107 -0
- janito/agent/queued_tool_handler.py +16 -0
- janito/agent/runtime_config.py +30 -0
- janito/agent/tool_handler.py +124 -0
- janito/agent/tools/__init__.py +11 -0
- janito/agent/tools/ask_user.py +63 -0
- janito/agent/tools/bash_exec.py +58 -0
- janito/agent/tools/create_directory.py +19 -0
- janito/agent/tools/create_file.py +43 -0
- janito/agent/tools/fetch_url.py +48 -0
- janito/agent/tools/file_str_replace.py +48 -0
- janito/agent/tools/find_files.py +37 -0
- janito/agent/tools/gitignore_utils.py +40 -0
- janito/agent/tools/move_file.py +37 -0
- janito/agent/tools/remove_file.py +19 -0
- janito/agent/tools/rich_live.py +37 -0
- janito/agent/tools/rich_utils.py +31 -0
- janito/agent/tools/search_text.py +41 -0
- janito/agent/tools/view_file.py +34 -0
- janito/cli/__init__.py +0 -6
- janito/cli/_print_config.py +68 -0
- janito/cli/_utils.py +8 -0
- janito/cli/arg_parser.py +26 -0
- janito/cli/config_commands.py +131 -0
- janito/cli/logging_setup.py +27 -0
- janito/cli/main.py +39 -0
- janito/cli/runner.py +135 -0
- janito/cli_chat_shell/__init__.py +1 -0
- janito/cli_chat_shell/chat_loop.py +147 -0
- janito/cli_chat_shell/commands.py +202 -0
- janito/cli_chat_shell/config_shell.py +75 -0
- janito/cli_chat_shell/load_prompt.py +15 -0
- janito/cli_chat_shell/session_manager.py +60 -0
- janito/cli_chat_shell/ui.py +136 -0
- janito/render_prompt.py +12 -0
- janito/templates/system_instructions.j2 +36 -0
- janito/web/__init__.py +0 -0
- janito/web/__main__.py +17 -0
- janito/web/app.py +132 -0
- janito-1.0.0.dist-info/METADATA +144 -0
- janito-1.0.0.dist-info/RECORD +51 -0
- {janito-0.14.0.dist-info → janito-1.0.0.dist-info}/WHEEL +2 -1
- janito-1.0.0.dist-info/entry_points.txt +2 -0
- {janito-0.14.0.dist-info → janito-1.0.0.dist-info}/licenses/LICENSE +2 -2
- janito-1.0.0.dist-info/top_level.txt +1 -0
- janito/callbacks.py +0 -34
- janito/cli/agent/__init__.py +0 -7
- janito/cli/agent/conversation.py +0 -149
- janito/cli/agent/initialization.py +0 -172
- janito/cli/agent/query.py +0 -108
- janito/cli/agent.py +0 -12
- janito/cli/app.py +0 -182
- janito/cli/commands/__init__.py +0 -12
- janito/cli/commands/config.py +0 -242
- janito/cli/commands/history.py +0 -119
- janito/cli/commands/profile.py +0 -72
- janito/cli/commands/validation.py +0 -24
- janito/cli/commands/workspace.py +0 -31
- janito/cli/commands.py +0 -12
- janito/cli/output.py +0 -29
- janito/cli/utils.py +0 -22
- janito/config.py +0 -375
- janito/data/instructions_template.txt +0 -31
- janito/token_report.py +0 -154
- janito/tools/__init__.py +0 -44
- janito/tools/bash/bash.py +0 -84
- janito/tools/bash/unix_persistent_bash.py +0 -184
- janito/tools/bash/win_persistent_bash.py +0 -308
- janito/tools/decorators.py +0 -90
- janito/tools/delete_file.py +0 -65
- janito/tools/fetch_webpage/__init__.py +0 -23
- janito/tools/fetch_webpage/core.py +0 -182
- janito/tools/find_files.py +0 -220
- janito/tools/move_file.py +0 -72
- janito/tools/prompt_user.py +0 -57
- janito/tools/replace_file.py +0 -63
- janito/tools/rich_console.py +0 -176
- janito/tools/search_text.py +0 -226
- janito/tools/str_replace_editor/__init__.py +0 -6
- janito/tools/str_replace_editor/editor.py +0 -55
- janito/tools/str_replace_editor/handlers/__init__.py +0 -16
- janito/tools/str_replace_editor/handlers/create.py +0 -60
- janito/tools/str_replace_editor/handlers/insert.py +0 -100
- janito/tools/str_replace_editor/handlers/str_replace.py +0 -94
- janito/tools/str_replace_editor/handlers/undo.py +0 -64
- janito/tools/str_replace_editor/handlers/view.py +0 -159
- janito/tools/str_replace_editor/utils.py +0 -33
- janito/tools/think.py +0 -37
- janito/tools/usage_tracker.py +0 -137
- janito-0.14.0.dist-info/METADATA +0 -396
- janito-0.14.0.dist-info/RECORD +0 -53
- janito-0.14.0.dist-info/entry_points.txt +0 -2
janito/tools/bash/bash.py
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
from typing import Optional
|
2
|
-
from typing import Tuple
|
3
|
-
import threading
|
4
|
-
import platform
|
5
|
-
import re
|
6
|
-
from janito.config import get_config
|
7
|
-
from janito.tools.usage_tracker import get_tracker
|
8
|
-
from janito.tools.rich_console import console, print_info
|
9
|
-
|
10
|
-
# Import the appropriate implementation based on the platform
|
11
|
-
if platform.system() == "Windows":
|
12
|
-
from janito.tools.bash.win_persistent_bash import PersistentBash
|
13
|
-
else:
|
14
|
-
from janito.tools.bash.unix_persistent_bash import PersistentBash
|
15
|
-
|
16
|
-
# Global instance of PersistentBash to maintain state between calls
|
17
|
-
_bash_session = None
|
18
|
-
_session_lock = threading.RLock() # Use RLock to allow reentrant locking
|
19
|
-
|
20
|
-
def bash_tool(command: str, restart: Optional[bool] = False) -> Tuple[str, bool]:
|
21
|
-
"""
|
22
|
-
Execute a bash command using a persistent Bash session.
|
23
|
-
The appropriate implementation (Windows or Unix) is selected based on the detected platform.
|
24
|
-
When in ask mode, only read-only commands are allowed.
|
25
|
-
Output is printed to the console in real-time as it's received.
|
26
|
-
|
27
|
-
Args:
|
28
|
-
command: The bash command to execute
|
29
|
-
restart: Whether to restart the bash session
|
30
|
-
|
31
|
-
Returns:
|
32
|
-
A tuple containing (output message, is_error flag)
|
33
|
-
"""
|
34
|
-
# Import console for printing output in real-time
|
35
|
-
from janito.tools.rich_console import console, print_info
|
36
|
-
|
37
|
-
# Only print command if not in trust mode
|
38
|
-
if not get_config().trust_mode:
|
39
|
-
print_info(f"{command}", "Bash Run")
|
40
|
-
global _bash_session
|
41
|
-
|
42
|
-
# Check if in ask mode and if the command might modify files
|
43
|
-
if get_config().ask_mode:
|
44
|
-
# List of potentially modifying commands
|
45
|
-
modifying_patterns = [
|
46
|
-
r'\brm\b', r'\bmkdir\b', r'\btouch\b', r'\becho\b.*[>\|]', r'\bmv\b', r'\bcp\b',
|
47
|
-
r'\bchmod\b', r'\bchown\b', r'\bsed\b.*-i', r'\bawk\b.*[>\|]', r'\bcat\b.*[>\|]',
|
48
|
-
r'\bwrite\b', r'\binstall\b', r'\bapt\b', r'\byum\b', r'\bpip\b.*install',
|
49
|
-
r'\bnpm\b.*install', r'\bdocker\b', r'\bkubectl\b.*apply', r'\bgit\b.*commit',
|
50
|
-
r'\bgit\b.*push', r'\bgit\b.*merge', r'\bdd\b'
|
51
|
-
]
|
52
|
-
|
53
|
-
# Check if command matches any modifying pattern
|
54
|
-
for pattern in modifying_patterns:
|
55
|
-
if re.search(pattern, command, re.IGNORECASE):
|
56
|
-
return ("Cannot execute potentially modifying commands in ask mode. Use --ask option to disable modifications.", True)
|
57
|
-
|
58
|
-
with _session_lock:
|
59
|
-
# Initialize or restart the session if needed
|
60
|
-
if _bash_session is None or restart:
|
61
|
-
if _bash_session is not None:
|
62
|
-
_bash_session.close()
|
63
|
-
# Get GitBash path from config (None means auto-detect)
|
64
|
-
gitbash_path = get_config().gitbash_path
|
65
|
-
_bash_session = PersistentBash(bash_path=gitbash_path)
|
66
|
-
|
67
|
-
try:
|
68
|
-
# Execute the command - output will be printed to console in real-time
|
69
|
-
output = _bash_session.execute(command)
|
70
|
-
|
71
|
-
# Track bash command execution
|
72
|
-
get_tracker().increment('bash_commands')
|
73
|
-
|
74
|
-
# Always assume execution was successful
|
75
|
-
is_error = False
|
76
|
-
|
77
|
-
# Return the output as a string (even though it was already printed in real-time)
|
78
|
-
return output, is_error
|
79
|
-
|
80
|
-
except Exception as e:
|
81
|
-
# Handle any exceptions that might occur
|
82
|
-
error_message = f"Error executing bash command: {str(e)}"
|
83
|
-
console.print(error_message, style="red bold")
|
84
|
-
return error_message, True
|
@@ -1,184 +0,0 @@
|
|
1
|
-
import subprocess
|
2
|
-
import time
|
3
|
-
import uuid
|
4
|
-
|
5
|
-
class PersistentBash:
|
6
|
-
"""
|
7
|
-
A wrapper class that maintains a persistent Bash session.
|
8
|
-
Allows sending commands and collecting output without restarting Bash.
|
9
|
-
"""
|
10
|
-
|
11
|
-
def __init__(self, bash_path=None):
|
12
|
-
"""
|
13
|
-
Initialize a persistent Bash session.
|
14
|
-
|
15
|
-
Args:
|
16
|
-
bash_path (str, optional): Path to the Bash executable. If None, tries to detect automatically.
|
17
|
-
"""
|
18
|
-
self.process = None
|
19
|
-
self.bash_path = bash_path
|
20
|
-
|
21
|
-
# If bash_path is not provided, try to detect it
|
22
|
-
if self.bash_path is None:
|
23
|
-
# On Unix-like systems, bash is usually in the PATH
|
24
|
-
self.bash_path = "bash"
|
25
|
-
|
26
|
-
# Check if bash exists
|
27
|
-
try:
|
28
|
-
subprocess.run(["which", "bash"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
29
|
-
except subprocess.CalledProcessError as err:
|
30
|
-
raise FileNotFoundError("Could not find bash executable. Please specify the path manually.") from err
|
31
|
-
|
32
|
-
# Start the bash process
|
33
|
-
self.start_process()
|
34
|
-
|
35
|
-
def start_process(self):
|
36
|
-
"""Start the Bash process."""
|
37
|
-
# Create a subprocess with pipe for stdin, stdout, and stderr
|
38
|
-
bash_args = [self.bash_path]
|
39
|
-
|
40
|
-
self.process = subprocess.Popen(
|
41
|
-
bash_args,
|
42
|
-
stdin=subprocess.PIPE,
|
43
|
-
stdout=subprocess.PIPE,
|
44
|
-
stderr=subprocess.STDOUT, # Redirect stderr to stdout
|
45
|
-
text=True, # Use text mode for input/output
|
46
|
-
bufsize=0, # Unbuffered
|
47
|
-
universal_newlines=True, # Universal newlines mode
|
48
|
-
)
|
49
|
-
|
50
|
-
# Set up a more reliable environment
|
51
|
-
setup_commands = [
|
52
|
-
"export PS1='$ '", # Simple prompt to avoid parsing issues
|
53
|
-
"export TERM=dumb", # Disable color codes and other terminal features
|
54
|
-
"set +o history", # Disable history
|
55
|
-
"shopt -s expand_aliases", # Enable alias expansion
|
56
|
-
]
|
57
|
-
|
58
|
-
# Send setup commands
|
59
|
-
for cmd in setup_commands:
|
60
|
-
self._send_command(cmd)
|
61
|
-
|
62
|
-
# Clear initial output with a marker
|
63
|
-
marker = f"INIT_COMPLETE_{uuid.uuid4().hex}"
|
64
|
-
self._send_command(f"echo {marker}")
|
65
|
-
|
66
|
-
while True:
|
67
|
-
line = self.process.stdout.readline().strip()
|
68
|
-
if marker in line:
|
69
|
-
break
|
70
|
-
|
71
|
-
def _send_command(self, command):
|
72
|
-
"""Send a command to the Bash process without reading the output."""
|
73
|
-
if self.process is None or self.process.poll() is not None:
|
74
|
-
self.start_process()
|
75
|
-
|
76
|
-
self.process.stdin.write(command + "\n")
|
77
|
-
self.process.stdin.flush()
|
78
|
-
|
79
|
-
def execute(self, command, timeout=None):
|
80
|
-
"""
|
81
|
-
Execute a command in the Bash session and return the output.
|
82
|
-
|
83
|
-
Args:
|
84
|
-
command (str): The command to execute.
|
85
|
-
timeout (int, optional): Timeout in seconds. If None, no timeout is applied.
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
str: The command output.
|
89
|
-
"""
|
90
|
-
from janito.tools.rich_console import console
|
91
|
-
|
92
|
-
if self.process is None or self.process.poll() is not None:
|
93
|
-
# Process has terminated, restart it
|
94
|
-
self.start_process()
|
95
|
-
|
96
|
-
# Create a unique marker to identify the end of output
|
97
|
-
end_marker = f"END_OF_COMMAND_{uuid.uuid4().hex}"
|
98
|
-
|
99
|
-
# Construct the wrapped command with echo markers
|
100
|
-
# Only use timeout when explicitly requested
|
101
|
-
if timeout is not None and timeout > 0:
|
102
|
-
# Check if timeout command is available
|
103
|
-
is_timeout_available = False
|
104
|
-
try:
|
105
|
-
check_cmd = "command -v timeout > /dev/null 2>&1 && echo available || echo unavailable"
|
106
|
-
self._send_command(check_cmd)
|
107
|
-
for _ in range(10): # Read up to 10 lines to find the result
|
108
|
-
line = self.process.stdout.readline().strip()
|
109
|
-
if "available" in line:
|
110
|
-
is_timeout_available = True
|
111
|
-
break
|
112
|
-
elif "unavailable" in line:
|
113
|
-
is_timeout_available = False
|
114
|
-
break
|
115
|
-
except:
|
116
|
-
is_timeout_available = False
|
117
|
-
|
118
|
-
if is_timeout_available:
|
119
|
-
# For timeout to work with shell syntax, we need to use bash -c
|
120
|
-
escaped_command = command.replace('"', '\\"')
|
121
|
-
wrapped_command = f"timeout {timeout}s bash -c \"{escaped_command}\" 2>&1; echo '{end_marker}'"
|
122
|
-
else:
|
123
|
-
wrapped_command = f"{command} 2>&1; echo '{end_marker}'"
|
124
|
-
else:
|
125
|
-
wrapped_command = f"{command} 2>&1; echo '{end_marker}'"
|
126
|
-
|
127
|
-
# Send the command
|
128
|
-
self._send_command(wrapped_command)
|
129
|
-
|
130
|
-
# Collect output until the end marker is found
|
131
|
-
output_lines = []
|
132
|
-
start_time = time.time()
|
133
|
-
max_wait = timeout if timeout is not None else 3600 # Default to 1 hour if no timeout
|
134
|
-
|
135
|
-
while time.time() - start_time < max_wait + 5: # Add buffer time
|
136
|
-
try:
|
137
|
-
line = self.process.stdout.readline().rstrip('\r\n')
|
138
|
-
if end_marker in line:
|
139
|
-
break
|
140
|
-
|
141
|
-
# Print the output to the console in real-time if not in trust mode
|
142
|
-
if line:
|
143
|
-
from janito.config import get_config
|
144
|
-
if not get_config().trust_mode:
|
145
|
-
console.print(line)
|
146
|
-
|
147
|
-
output_lines.append(line)
|
148
|
-
except Exception as e:
|
149
|
-
error_msg = f"[Error reading output: {str(e)}]"
|
150
|
-
console.print(error_msg, style="red")
|
151
|
-
output_lines.append(error_msg)
|
152
|
-
continue
|
153
|
-
|
154
|
-
# Check for timeout
|
155
|
-
if time.time() - start_time >= max_wait + 5:
|
156
|
-
timeout_msg = f"Error: Command timed out after {max_wait} seconds"
|
157
|
-
console.print(timeout_msg, style="red bold")
|
158
|
-
output_lines.append(timeout_msg)
|
159
|
-
|
160
|
-
# Try to reset the bash session after a timeout
|
161
|
-
self.close()
|
162
|
-
self.start_process()
|
163
|
-
|
164
|
-
return "\n".join(output_lines)
|
165
|
-
|
166
|
-
def close(self):
|
167
|
-
"""Close the Bash session."""
|
168
|
-
if self.process and self.process.poll() is None:
|
169
|
-
try:
|
170
|
-
self._send_command("exit")
|
171
|
-
self.process.wait(timeout=2)
|
172
|
-
except:
|
173
|
-
pass
|
174
|
-
finally:
|
175
|
-
try:
|
176
|
-
self.process.terminate()
|
177
|
-
except:
|
178
|
-
pass
|
179
|
-
|
180
|
-
self.process = None
|
181
|
-
|
182
|
-
def __del__(self):
|
183
|
-
"""Destructor to ensure the process is closed."""
|
184
|
-
self.close()
|
@@ -1,308 +0,0 @@
|
|
1
|
-
import subprocess
|
2
|
-
import os
|
3
|
-
import platform
|
4
|
-
import time
|
5
|
-
import uuid
|
6
|
-
import sys
|
7
|
-
import io
|
8
|
-
import codecs
|
9
|
-
|
10
|
-
class PersistentBash:
|
11
|
-
"""
|
12
|
-
A wrapper class that maintains a persistent Bash session.
|
13
|
-
Allows sending commands and collecting output without restarting Bash.
|
14
|
-
"""
|
15
|
-
|
16
|
-
def __init__(self, bash_path=None):
|
17
|
-
"""
|
18
|
-
Initialize a persistent Bash session.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
bash_path (str, optional): Path to the Bash executable. If None, tries to detect automatically.
|
22
|
-
This can be configured in Janito's config using the gitbash_path setting.
|
23
|
-
"""
|
24
|
-
self.process = None
|
25
|
-
self.bash_path = bash_path
|
26
|
-
|
27
|
-
# Configure UTF-8 support for Windows
|
28
|
-
if platform.system() == "Windows":
|
29
|
-
# Force UTF-8 mode in Python 3.7+
|
30
|
-
os.environ["PYTHONUTF8"] = "1"
|
31
|
-
|
32
|
-
# Set Python's standard IO encoding to UTF-8
|
33
|
-
if hasattr(sys.stdout, 'reconfigure'):
|
34
|
-
sys.stdout.reconfigure(encoding='utf-8')
|
35
|
-
if hasattr(sys.stderr, 'reconfigure'):
|
36
|
-
sys.stderr.reconfigure(encoding='utf-8')
|
37
|
-
if hasattr(sys.stdin, 'reconfigure'):
|
38
|
-
sys.stdin.reconfigure(encoding='utf-8')
|
39
|
-
|
40
|
-
# Ensure Windows console is in UTF-8 mode
|
41
|
-
try:
|
42
|
-
# Try to set console mode to UTF-8
|
43
|
-
os.system("chcp 65001 > nul")
|
44
|
-
|
45
|
-
# Redirect stdout through a UTF-8 writer
|
46
|
-
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace', line_buffering=True)
|
47
|
-
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace', line_buffering=True)
|
48
|
-
except Exception as e:
|
49
|
-
print(f"Warning: Failed to set up UTF-8 encoding: {str(e)}")
|
50
|
-
|
51
|
-
# If bash_path is not provided, try to detect it
|
52
|
-
if self.bash_path is None:
|
53
|
-
if platform.system() == "Windows":
|
54
|
-
# Common paths for Git Bash on Windows
|
55
|
-
possible_paths = [
|
56
|
-
r"C:\Program Files\Git\bin\bash.exe",
|
57
|
-
r"C:\Program Files (x86)\Git\bin\bash.exe",
|
58
|
-
]
|
59
|
-
for path in possible_paths:
|
60
|
-
if os.path.exists(path):
|
61
|
-
self.bash_path = path
|
62
|
-
break
|
63
|
-
if self.bash_path is None:
|
64
|
-
raise FileNotFoundError("Could not find Git Bash executable. Please specify the path manually.")
|
65
|
-
else:
|
66
|
-
# On Unix-like systems, bash is usually in the PATH
|
67
|
-
self.bash_path = "bash"
|
68
|
-
|
69
|
-
# Start the bash process
|
70
|
-
self.start_process()
|
71
|
-
|
72
|
-
def start_process(self):
|
73
|
-
"""Start the Bash process."""
|
74
|
-
# Create a subprocess with pipe for stdin, stdout, and stderr
|
75
|
-
bash_args = [self.bash_path]
|
76
|
-
|
77
|
-
# Set UTF-8 codepage for Windows
|
78
|
-
env = os.environ.copy()
|
79
|
-
if platform.system() == "Windows":
|
80
|
-
# Set codepage to UTF-8 (65001) - run this before starting the process
|
81
|
-
os.system("chcp 65001 > nul")
|
82
|
-
# Set environment variables for proper UTF-8 handling
|
83
|
-
env["PYTHONIOENCODING"] = "utf-8"
|
84
|
-
env["PYTHONUTF8"] = "1"
|
85
|
-
# Add additional environment variables for Windows CMD
|
86
|
-
env["LANG"] = "en_US.UTF-8"
|
87
|
-
env["LC_ALL"] = "en_US.UTF-8"
|
88
|
-
|
89
|
-
# Create the process with binary pipes for better control over encoding
|
90
|
-
if platform.system() == "Windows":
|
91
|
-
# On Windows, we need special handling for UTF-8
|
92
|
-
self.process = subprocess.Popen(
|
93
|
-
bash_args,
|
94
|
-
stdin=subprocess.PIPE,
|
95
|
-
stdout=subprocess.PIPE,
|
96
|
-
stderr=subprocess.STDOUT, # Redirect stderr to stdout
|
97
|
-
bufsize=0, # Unbuffered
|
98
|
-
universal_newlines=False, # Use binary mode
|
99
|
-
env=env # Pass the modified environment
|
100
|
-
)
|
101
|
-
|
102
|
-
# Create UTF-8 wrappers for stdin/stdout
|
103
|
-
self.stdin = io.TextIOWrapper(self.process.stdin, encoding='utf-8', errors='replace', line_buffering=True)
|
104
|
-
self.stdout = io.TextIOWrapper(self.process.stdout, encoding='utf-8', errors='replace', line_buffering=True)
|
105
|
-
else:
|
106
|
-
# On Unix systems, the standard approach works fine
|
107
|
-
self.process = subprocess.Popen(
|
108
|
-
bash_args,
|
109
|
-
stdin=subprocess.PIPE,
|
110
|
-
stdout=subprocess.PIPE,
|
111
|
-
stderr=subprocess.STDOUT, # Redirect stderr to stdout
|
112
|
-
text=True, # Use text mode for input/output
|
113
|
-
bufsize=0, # Unbuffered
|
114
|
-
universal_newlines=True, # Universal newlines mode
|
115
|
-
env=env, # Pass the modified environment
|
116
|
-
encoding='utf-8', # Explicitly set encoding to UTF-8
|
117
|
-
errors='replace' # Replace invalid characters instead of failing
|
118
|
-
)
|
119
|
-
self.stdin = self.process.stdin
|
120
|
-
self.stdout = self.process.stdout
|
121
|
-
|
122
|
-
# Set up a more reliable environment
|
123
|
-
setup_commands = [
|
124
|
-
"export PS1='$ '", # Simple prompt to avoid parsing issues
|
125
|
-
"export TERM=dumb", # Disable color codes and other terminal features
|
126
|
-
"set +o history", # Disable history
|
127
|
-
"shopt -s expand_aliases", # Enable alias expansion
|
128
|
-
"export LANG=en_US.UTF-8", # Set UTF-8 locale
|
129
|
-
"export LC_ALL=en_US.UTF-8", # Set all locale categories to UTF-8
|
130
|
-
]
|
131
|
-
|
132
|
-
# Additional setup for Windows to handle UTF-8
|
133
|
-
if platform.system() == "Windows":
|
134
|
-
setup_commands.extend([
|
135
|
-
# Force Git Bash to use UTF-8
|
136
|
-
"export LESSCHARSET=utf-8",
|
137
|
-
# Ensure proper display of Unicode characters
|
138
|
-
"export PYTHONIOENCODING=utf-8"
|
139
|
-
])
|
140
|
-
|
141
|
-
# Send setup commands
|
142
|
-
for cmd in setup_commands:
|
143
|
-
self._send_command(cmd)
|
144
|
-
|
145
|
-
# Clear initial output with a marker
|
146
|
-
marker = f"INIT_COMPLETE_{uuid.uuid4().hex}"
|
147
|
-
self._send_command(f"echo {marker}")
|
148
|
-
|
149
|
-
while True:
|
150
|
-
line = self.stdout.readline().strip()
|
151
|
-
if marker in line:
|
152
|
-
break
|
153
|
-
|
154
|
-
def _send_command(self, command):
|
155
|
-
"""Send a command to the Bash process without reading the output."""
|
156
|
-
if self.process is None or self.process.poll() is not None:
|
157
|
-
self.start_process()
|
158
|
-
|
159
|
-
# Use our stdin wrapper instead of process.stdin directly
|
160
|
-
self.stdin.write(command + "\n")
|
161
|
-
self.stdin.flush()
|
162
|
-
|
163
|
-
def execute(self, command, timeout=None):
|
164
|
-
"""
|
165
|
-
Execute a command in the Bash session and return the output.
|
166
|
-
|
167
|
-
Args:
|
168
|
-
command (str): The command to execute.
|
169
|
-
timeout (int, optional): Timeout in seconds. If None, no timeout is applied.
|
170
|
-
|
171
|
-
Returns:
|
172
|
-
str: The command output.
|
173
|
-
"""
|
174
|
-
if self.process is None or self.process.poll() is not None:
|
175
|
-
# Process has terminated, restart it
|
176
|
-
self.start_process()
|
177
|
-
|
178
|
-
# Create a unique marker to identify the end of output
|
179
|
-
end_marker = f"END_OF_COMMAND_{uuid.uuid4().hex}"
|
180
|
-
|
181
|
-
# Construct the wrapped command with echo markers
|
182
|
-
# Only use timeout when explicitly requested
|
183
|
-
if timeout is not None and timeout > 0:
|
184
|
-
# Check if timeout command is available
|
185
|
-
is_timeout_available = False
|
186
|
-
try:
|
187
|
-
check_cmd = "command -v timeout > /dev/null 2>&1 && echo available || echo unavailable"
|
188
|
-
self._send_command(check_cmd)
|
189
|
-
for _ in range(10): # Read up to 10 lines to find the result
|
190
|
-
line = self.stdout.readline().strip()
|
191
|
-
if "available" in line:
|
192
|
-
is_timeout_available = True
|
193
|
-
break
|
194
|
-
elif "unavailable" in line:
|
195
|
-
is_timeout_available = False
|
196
|
-
break
|
197
|
-
except:
|
198
|
-
is_timeout_available = False
|
199
|
-
|
200
|
-
if is_timeout_available:
|
201
|
-
# For timeout to work with shell syntax, we need to use bash -c
|
202
|
-
wrapped_command = f"timeout {timeout}s bash -c \"{command.replace('"', '\\"')}\" 2>&1; echo '{end_marker}'"
|
203
|
-
else:
|
204
|
-
wrapped_command = f"{command} 2>&1; echo '{end_marker}'"
|
205
|
-
else:
|
206
|
-
wrapped_command = f"{command} 2>&1; echo '{end_marker}'"
|
207
|
-
|
208
|
-
# Send the command
|
209
|
-
self._send_command(wrapped_command)
|
210
|
-
|
211
|
-
# Import the console here to avoid circular imports
|
212
|
-
from janito.tools.rich_console import console
|
213
|
-
|
214
|
-
# Collect output until the end marker is found
|
215
|
-
output_lines = []
|
216
|
-
start_time = time.time()
|
217
|
-
max_wait = timeout if timeout is not None else 3600 # Default to 1 hour if no timeout
|
218
|
-
|
219
|
-
while time.time() - start_time < max_wait + 5: # Add buffer time
|
220
|
-
try:
|
221
|
-
line = self.stdout.readline().rstrip('\r\n')
|
222
|
-
if end_marker in line:
|
223
|
-
break
|
224
|
-
|
225
|
-
# Print the output to the console in real-time if not in trust mode
|
226
|
-
if line:
|
227
|
-
from janito.config import get_config
|
228
|
-
if not get_config().trust_mode:
|
229
|
-
console.print(line)
|
230
|
-
|
231
|
-
output_lines.append(line)
|
232
|
-
except UnicodeDecodeError as e:
|
233
|
-
# Handle potential UTF-8 decoding errors
|
234
|
-
error_msg = f"[Warning: Unicode decode error occurred: {str(e)}]"
|
235
|
-
console.print(error_msg, style="yellow")
|
236
|
-
output_lines.append(error_msg)
|
237
|
-
# Just continue with replacement character
|
238
|
-
continue
|
239
|
-
except Exception as e:
|
240
|
-
error_msg = f"[Error reading output: {str(e)}]"
|
241
|
-
console.print(error_msg, style="red")
|
242
|
-
output_lines.append(error_msg)
|
243
|
-
continue
|
244
|
-
|
245
|
-
# Check for timeout
|
246
|
-
if time.time() - start_time >= max_wait + 5:
|
247
|
-
timeout_msg = f"Error: Command timed out after {max_wait} seconds"
|
248
|
-
console.print(timeout_msg, style="red bold")
|
249
|
-
output_lines.append(timeout_msg)
|
250
|
-
|
251
|
-
# Try to reset the bash session after a timeout
|
252
|
-
self.close()
|
253
|
-
self.start_process()
|
254
|
-
|
255
|
-
return "\n".join(output_lines)
|
256
|
-
|
257
|
-
def windows_to_bash_path(self, windows_path):
|
258
|
-
"""
|
259
|
-
Convert a Windows path to a Git Bash compatible path.
|
260
|
-
|
261
|
-
Args:
|
262
|
-
windows_path (str): A Windows path like 'C:\\folder\\file.txt'
|
263
|
-
|
264
|
-
Returns:
|
265
|
-
str: Git Bash compatible path like '/c/folder/file.txt'
|
266
|
-
"""
|
267
|
-
if not windows_path or not platform.system() == "Windows":
|
268
|
-
return windows_path
|
269
|
-
|
270
|
-
# Handle drive letter (e.g., C: -> /c)
|
271
|
-
if ":" in windows_path:
|
272
|
-
drive, path = windows_path.split(":", 1)
|
273
|
-
unix_path = f"/{drive.lower()}{path}"
|
274
|
-
else:
|
275
|
-
unix_path = windows_path
|
276
|
-
|
277
|
-
# Convert backslashes to forward slashes
|
278
|
-
unix_path = unix_path.replace("\\", "/")
|
279
|
-
|
280
|
-
# Remove any double slashes
|
281
|
-
while "//" in unix_path:
|
282
|
-
unix_path = unix_path.replace("//", "/")
|
283
|
-
|
284
|
-
# If the path contains spaces, we need to escape them or quote the entire path
|
285
|
-
if " " in unix_path:
|
286
|
-
unix_path = f'"{unix_path}"'
|
287
|
-
|
288
|
-
return unix_path
|
289
|
-
|
290
|
-
def close(self):
|
291
|
-
"""Close the Bash session."""
|
292
|
-
if self.process and self.process.poll() is None:
|
293
|
-
try:
|
294
|
-
self._send_command("exit")
|
295
|
-
self.process.wait(timeout=2)
|
296
|
-
except:
|
297
|
-
pass
|
298
|
-
finally:
|
299
|
-
try:
|
300
|
-
self.process.terminate()
|
301
|
-
except:
|
302
|
-
pass
|
303
|
-
|
304
|
-
self.process = None
|
305
|
-
|
306
|
-
def __del__(self):
|
307
|
-
"""Destructor to ensure the process is closed."""
|
308
|
-
self.close()
|
janito/tools/decorators.py
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Decorators for janito tools.
|
3
|
-
"""
|
4
|
-
import functools
|
5
|
-
import string
|
6
|
-
from typing import Any, Callable, Dict, Optional
|
7
|
-
|
8
|
-
|
9
|
-
class ToolMetaFormatter(string.Formatter):
|
10
|
-
"""Custom string formatter that handles conditional expressions in format strings."""
|
11
|
-
|
12
|
-
def get_value(self, key, args, kwargs):
|
13
|
-
"""Override to handle conditional expressions."""
|
14
|
-
if key in kwargs:
|
15
|
-
return kwargs[key]
|
16
|
-
|
17
|
-
# Try to evaluate the key as a Python expression
|
18
|
-
try:
|
19
|
-
# Create a safe local namespace with only the parameters
|
20
|
-
return eval(key, {"__builtins__": {}}, kwargs)
|
21
|
-
except Exception:
|
22
|
-
return f"[{key}]"
|
23
|
-
|
24
|
-
|
25
|
-
def tool_meta(label: str):
|
26
|
-
"""
|
27
|
-
Decorator to add metadata to a tool function.
|
28
|
-
|
29
|
-
Args:
|
30
|
-
label: A format string that can reference function parameters.
|
31
|
-
Example: "Finding files {pattern}, on {root_dir}"
|
32
|
-
|
33
|
-
Returns:
|
34
|
-
Decorated function with metadata attached
|
35
|
-
"""
|
36
|
-
def decorator(func: Callable):
|
37
|
-
@functools.wraps(func)
|
38
|
-
def wrapper(*args, **kwargs):
|
39
|
-
return func(*args, **kwargs)
|
40
|
-
|
41
|
-
# Attach metadata to the function
|
42
|
-
wrapper._tool_meta = {
|
43
|
-
'label': label
|
44
|
-
}
|
45
|
-
|
46
|
-
return wrapper
|
47
|
-
|
48
|
-
return decorator
|
49
|
-
|
50
|
-
|
51
|
-
def tool(func: Callable):
|
52
|
-
"""
|
53
|
-
Basic decorator for tool functions.
|
54
|
-
|
55
|
-
This decorator marks a function as a tool and can be used for
|
56
|
-
simpler tools that don't need additional metadata.
|
57
|
-
|
58
|
-
Returns:
|
59
|
-
Decorated function
|
60
|
-
"""
|
61
|
-
@functools.wraps(func)
|
62
|
-
def wrapper(*args, **kwargs):
|
63
|
-
return func(*args, **kwargs)
|
64
|
-
|
65
|
-
return wrapper
|
66
|
-
|
67
|
-
|
68
|
-
def format_tool_label(func: Callable, tool_input: Dict[str, Any]) -> Optional[str]:
|
69
|
-
"""
|
70
|
-
Format the tool label using the function's parameters.
|
71
|
-
|
72
|
-
Args:
|
73
|
-
func: The tool function
|
74
|
-
tool_input: Input parameters for the tool
|
75
|
-
|
76
|
-
Returns:
|
77
|
-
Formatted label string or None if no label is defined
|
78
|
-
"""
|
79
|
-
if not hasattr(func, '_tool_meta') or 'label' not in func._tool_meta:
|
80
|
-
return None
|
81
|
-
|
82
|
-
# Get the label template
|
83
|
-
label_template = func._tool_meta['label']
|
84
|
-
|
85
|
-
# Format the label with the parameters
|
86
|
-
try:
|
87
|
-
formatter = ToolMetaFormatter()
|
88
|
-
return formatter.format(label_template, **tool_input)
|
89
|
-
except Exception:
|
90
|
-
return f"{func.__name__}"
|