janito 0.15.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.15.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.15.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 -168
- janito/cli/agent/query.py +0 -112
- janito/cli/agent.py +0 -12
- janito/cli/app.py +0 -178
- janito/cli/commands/__init__.py +0 -12
- janito/cli/commands/config.py +0 -30
- janito/cli/commands/history.py +0 -119
- janito/cli/commands/profile.py +0 -93
- 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/README.md +0 -104
- janito/config/__init__.py +0 -16
- janito/config/cli/__init__.py +0 -28
- janito/config/cli/commands.py +0 -397
- janito/config/cli/validators.py +0 -77
- janito/config/core/__init__.py +0 -23
- janito/config/core/file_operations.py +0 -90
- janito/config/core/properties.py +0 -316
- janito/config/core/singleton.py +0 -282
- janito/config/profiles/__init__.py +0 -8
- janito/config/profiles/definitions.py +0 -38
- janito/config/profiles/manager.py +0 -80
- janito/data/instructions_template.txt +0 -34
- janito/token_report.py +0 -154
- janito/tools/__init__.py +0 -44
- janito/tools/bash/bash.py +0 -157
- janito/tools/bash/unix_persistent_bash.py +0 -215
- janito/tools/bash/win_persistent_bash.py +0 -341
- 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 -165
- janito/tools/str_replace_editor/utils.py +0 -33
- janito/tools/think.py +0 -37
- janito/tools/usage_tracker.py +0 -137
- janito-0.15.0.dist-info/METADATA +0 -481
- janito-0.15.0.dist-info/RECORD +0 -64
- janito-0.15.0.dist-info/entry_points.txt +0 -2
@@ -1,215 +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
|
-
# Check if we're being run from the main bash_tool function
|
136
|
-
# which will handle interruption
|
137
|
-
try:
|
138
|
-
from janito.tools.bash.bash import _command_interrupted
|
139
|
-
except ImportError:
|
140
|
-
_command_interrupted = False
|
141
|
-
|
142
|
-
while time.time() - start_time < max_wait + 5: # Add buffer time
|
143
|
-
# Check if we've been interrupted
|
144
|
-
if '_command_interrupted' in globals() and _command_interrupted:
|
145
|
-
# Send Ctrl+C to the running process
|
146
|
-
if self.process and self.process.poll() is None:
|
147
|
-
try:
|
148
|
-
# Send interrupt signal to the process group
|
149
|
-
import os
|
150
|
-
import signal
|
151
|
-
pgid = os.getpgid(self.process.pid)
|
152
|
-
os.killpg(pgid, signal.SIGINT)
|
153
|
-
except:
|
154
|
-
pass
|
155
|
-
|
156
|
-
# Add message to output
|
157
|
-
interrupt_msg = "Command interrupted by user (Ctrl+C)"
|
158
|
-
console.print(f"[bold red]{interrupt_msg}[/bold red]")
|
159
|
-
output_lines.append(interrupt_msg)
|
160
|
-
|
161
|
-
# Reset the bash session
|
162
|
-
self.close()
|
163
|
-
self.start_process()
|
164
|
-
|
165
|
-
break
|
166
|
-
|
167
|
-
try:
|
168
|
-
line = self.process.stdout.readline().rstrip('\r\n')
|
169
|
-
if end_marker in line:
|
170
|
-
break
|
171
|
-
|
172
|
-
# Print the output to the console in real-time if not in trust mode
|
173
|
-
if line:
|
174
|
-
from janito.config import get_config
|
175
|
-
if not get_config().trust_mode:
|
176
|
-
console.print(line)
|
177
|
-
|
178
|
-
output_lines.append(line)
|
179
|
-
except Exception as e:
|
180
|
-
error_msg = f"[Error reading output: {str(e)}]"
|
181
|
-
console.print(error_msg, style="red")
|
182
|
-
output_lines.append(error_msg)
|
183
|
-
continue
|
184
|
-
|
185
|
-
# Check for timeout
|
186
|
-
if time.time() - start_time >= max_wait + 5 and not _command_interrupted:
|
187
|
-
timeout_msg = f"Error: Command timed out after {max_wait} seconds"
|
188
|
-
console.print(timeout_msg, style="red bold")
|
189
|
-
output_lines.append(timeout_msg)
|
190
|
-
|
191
|
-
# Try to reset the bash session after a timeout
|
192
|
-
self.close()
|
193
|
-
self.start_process()
|
194
|
-
|
195
|
-
return "\n".join(output_lines)
|
196
|
-
|
197
|
-
def close(self):
|
198
|
-
"""Close the Bash session."""
|
199
|
-
if self.process and self.process.poll() is None:
|
200
|
-
try:
|
201
|
-
self._send_command("exit")
|
202
|
-
self.process.wait(timeout=2)
|
203
|
-
except:
|
204
|
-
pass
|
205
|
-
finally:
|
206
|
-
try:
|
207
|
-
self.process.terminate()
|
208
|
-
except:
|
209
|
-
pass
|
210
|
-
|
211
|
-
self.process = None
|
212
|
-
|
213
|
-
def __del__(self):
|
214
|
-
"""Destructor to ensure the process is closed."""
|
215
|
-
self.close()
|
@@ -1,341 +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
|
-
# Check if we're being run from the main bash_tool function
|
220
|
-
# which will handle interruption
|
221
|
-
try:
|
222
|
-
from janito.tools.bash.bash import _command_interrupted
|
223
|
-
except ImportError:
|
224
|
-
_command_interrupted = False
|
225
|
-
|
226
|
-
while time.time() - start_time < max_wait + 5: # Add buffer time
|
227
|
-
# Check if we've been interrupted
|
228
|
-
if '_command_interrupted' in globals() and _command_interrupted:
|
229
|
-
# Send Ctrl+C to the running process
|
230
|
-
if self.process and self.process.poll() is None:
|
231
|
-
try:
|
232
|
-
# On Windows, we need to use CTRL_C_EVENT
|
233
|
-
import signal
|
234
|
-
self.process.send_signal(signal.CTRL_C_EVENT)
|
235
|
-
except:
|
236
|
-
# If that fails, try to terminate the process
|
237
|
-
try:
|
238
|
-
self.process.terminate()
|
239
|
-
except:
|
240
|
-
pass
|
241
|
-
|
242
|
-
# Add message to output
|
243
|
-
interrupt_msg = "Command interrupted by user (Ctrl+C)"
|
244
|
-
console.print(f"[bold red]{interrupt_msg}[/bold red]")
|
245
|
-
output_lines.append(interrupt_msg)
|
246
|
-
|
247
|
-
# Reset the bash session
|
248
|
-
self.close()
|
249
|
-
self.start_process()
|
250
|
-
|
251
|
-
break
|
252
|
-
|
253
|
-
try:
|
254
|
-
line = self.stdout.readline().rstrip('\r\n')
|
255
|
-
if end_marker in line:
|
256
|
-
break
|
257
|
-
|
258
|
-
# Print the output to the console in real-time if not in trust mode
|
259
|
-
if line:
|
260
|
-
from janito.config import get_config
|
261
|
-
if not get_config().trust_mode:
|
262
|
-
console.print(line)
|
263
|
-
|
264
|
-
output_lines.append(line)
|
265
|
-
except UnicodeDecodeError as e:
|
266
|
-
# Handle potential UTF-8 decoding errors
|
267
|
-
error_msg = f"[Warning: Unicode decode error occurred: {str(e)}]"
|
268
|
-
console.print(error_msg, style="yellow")
|
269
|
-
output_lines.append(error_msg)
|
270
|
-
# Just continue with replacement character
|
271
|
-
continue
|
272
|
-
except Exception as e:
|
273
|
-
error_msg = f"[Error reading output: {str(e)}]"
|
274
|
-
console.print(error_msg, style="red")
|
275
|
-
output_lines.append(error_msg)
|
276
|
-
continue
|
277
|
-
|
278
|
-
# Check for timeout
|
279
|
-
if time.time() - start_time >= max_wait + 5 and not _command_interrupted:
|
280
|
-
timeout_msg = f"Error: Command timed out after {max_wait} seconds"
|
281
|
-
console.print(timeout_msg, style="red bold")
|
282
|
-
output_lines.append(timeout_msg)
|
283
|
-
|
284
|
-
# Try to reset the bash session after a timeout
|
285
|
-
self.close()
|
286
|
-
self.start_process()
|
287
|
-
|
288
|
-
return "\n".join(output_lines)
|
289
|
-
|
290
|
-
def windows_to_bash_path(self, windows_path):
|
291
|
-
"""
|
292
|
-
Convert a Windows path to a Git Bash compatible path.
|
293
|
-
|
294
|
-
Args:
|
295
|
-
windows_path (str): A Windows path like 'C:\\folder\\file.txt'
|
296
|
-
|
297
|
-
Returns:
|
298
|
-
str: Git Bash compatible path like '/c/folder/file.txt'
|
299
|
-
"""
|
300
|
-
if not windows_path or not platform.system() == "Windows":
|
301
|
-
return windows_path
|
302
|
-
|
303
|
-
# Handle drive letter (e.g., C: -> /c)
|
304
|
-
if ":" in windows_path:
|
305
|
-
drive, path = windows_path.split(":", 1)
|
306
|
-
unix_path = f"/{drive.lower()}{path}"
|
307
|
-
else:
|
308
|
-
unix_path = windows_path
|
309
|
-
|
310
|
-
# Convert backslashes to forward slashes
|
311
|
-
unix_path = unix_path.replace("\\", "/")
|
312
|
-
|
313
|
-
# Remove any double slashes
|
314
|
-
while "//" in unix_path:
|
315
|
-
unix_path = unix_path.replace("//", "/")
|
316
|
-
|
317
|
-
# If the path contains spaces, we need to escape them or quote the entire path
|
318
|
-
if " " in unix_path:
|
319
|
-
unix_path = f'"{unix_path}"'
|
320
|
-
|
321
|
-
return unix_path
|
322
|
-
|
323
|
-
def close(self):
|
324
|
-
"""Close the Bash session."""
|
325
|
-
if self.process and self.process.poll() is None:
|
326
|
-
try:
|
327
|
-
self._send_command("exit")
|
328
|
-
self.process.wait(timeout=2)
|
329
|
-
except:
|
330
|
-
pass
|
331
|
-
finally:
|
332
|
-
try:
|
333
|
-
self.process.terminate()
|
334
|
-
except:
|
335
|
-
pass
|
336
|
-
|
337
|
-
self.process = None
|
338
|
-
|
339
|
-
def __del__(self):
|
340
|
-
"""Destructor to ensure the process is closed."""
|
341
|
-
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__}"
|