janito 0.14.0__py3-none-any.whl → 0.15.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/cli/agent/initialization.py +4 -8
- janito/cli/agent/query.py +29 -25
- janito/cli/app.py +17 -21
- janito/cli/commands/config.py +25 -237
- janito/cli/commands/profile.py +92 -71
- janito/cli/commands/workspace.py +30 -30
- janito/config/README.md +104 -0
- janito/config/__init__.py +16 -0
- janito/config/cli/__init__.py +28 -0
- janito/config/cli/commands.py +397 -0
- janito/config/cli/validators.py +77 -0
- janito/config/core/__init__.py +23 -0
- janito/config/core/file_operations.py +90 -0
- janito/config/core/properties.py +316 -0
- janito/config/core/singleton.py +282 -0
- janito/config/profiles/__init__.py +8 -0
- janito/config/profiles/definitions.py +38 -0
- janito/config/profiles/manager.py +80 -0
- janito/data/instructions_template.txt +6 -3
- janito/tools/bash/bash.py +80 -7
- janito/tools/bash/unix_persistent_bash.py +32 -1
- janito/tools/bash/win_persistent_bash.py +34 -1
- janito/tools/move_file.py +1 -1
- janito/tools/str_replace_editor/handlers/view.py +14 -8
- {janito-0.14.0.dist-info → janito-0.15.0.dist-info}/METADATA +107 -22
- {janito-0.14.0.dist-info → janito-0.15.0.dist-info}/RECORD +31 -20
- janito/config.py +0 -375
- {janito-0.14.0.dist-info → janito-0.15.0.dist-info}/WHEEL +0 -0
- {janito-0.14.0.dist-info → janito-0.15.0.dist-info}/entry_points.txt +0 -0
- {janito-0.14.0.dist-info → janito-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,282 @@
|
|
1
|
+
"""
|
2
|
+
Singleton implementation of the Config class for Janito.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
from typing import Dict, Any, Optional, Union
|
6
|
+
|
7
|
+
from .properties import ConfigProperties
|
8
|
+
from .file_operations import (
|
9
|
+
get_global_config_path,
|
10
|
+
get_local_config_path,
|
11
|
+
load_config_file,
|
12
|
+
save_config_file,
|
13
|
+
merge_configs
|
14
|
+
)
|
15
|
+
from ..profiles.manager import get_profile
|
16
|
+
from ..profiles.definitions import PROFILES
|
17
|
+
|
18
|
+
class Config(ConfigProperties):
|
19
|
+
"""Singleton configuration class for Janito."""
|
20
|
+
_instance = None
|
21
|
+
|
22
|
+
def __new__(cls):
|
23
|
+
if cls._instance is None:
|
24
|
+
cls._instance = super(Config, cls).__new__(cls)
|
25
|
+
cls._instance._workspace_dir = os.getcwd()
|
26
|
+
cls._instance._verbose = False
|
27
|
+
cls._instance._ask_mode = False
|
28
|
+
cls._instance._trust_mode = False
|
29
|
+
cls._instance._no_tools = False
|
30
|
+
cls._instance._show_usage_report = True # Enabled by default
|
31
|
+
|
32
|
+
# Set technical profile as default
|
33
|
+
profile_data = PROFILES["technical"]
|
34
|
+
cls._instance._temperature = profile_data["temperature"]
|
35
|
+
cls._instance._profile = "technical"
|
36
|
+
cls._instance._role = "software engineer"
|
37
|
+
cls._instance._gitbash_path = None # Default to None for auto-detection
|
38
|
+
# Default max_view_lines will be retrieved from merged_config
|
39
|
+
|
40
|
+
# Initialize configuration storage
|
41
|
+
cls._instance._global_config = {}
|
42
|
+
cls._instance._local_config = {}
|
43
|
+
cls._instance._merged_config = {}
|
44
|
+
|
45
|
+
# Load configurations
|
46
|
+
cls._instance._load_config()
|
47
|
+
return cls._instance
|
48
|
+
|
49
|
+
def _load_config(self) -> None:
|
50
|
+
"""Load both global and local configurations and merge them."""
|
51
|
+
# Load global config
|
52
|
+
global_config_path = get_global_config_path()
|
53
|
+
self._global_config = load_config_file(global_config_path)
|
54
|
+
|
55
|
+
# Load local config
|
56
|
+
local_config_path = get_local_config_path(self._workspace_dir)
|
57
|
+
self._local_config = load_config_file(local_config_path)
|
58
|
+
|
59
|
+
# Remove runtime-only settings from config files if they exist
|
60
|
+
self._clean_runtime_settings()
|
61
|
+
|
62
|
+
# Merge configurations (local overrides global)
|
63
|
+
self._merge_configs()
|
64
|
+
|
65
|
+
# Apply merged configuration to instance variables
|
66
|
+
self._apply_config()
|
67
|
+
|
68
|
+
def _clean_runtime_settings(self) -> None:
|
69
|
+
"""Remove runtime-only settings from configuration files if they exist."""
|
70
|
+
runtime_settings = ["ask_mode"]
|
71
|
+
config_changed = False
|
72
|
+
|
73
|
+
# Remove from local config
|
74
|
+
for setting in runtime_settings:
|
75
|
+
if setting in self._local_config:
|
76
|
+
del self._local_config[setting]
|
77
|
+
config_changed = True
|
78
|
+
|
79
|
+
# Remove from global config
|
80
|
+
for setting in runtime_settings:
|
81
|
+
if setting in self._global_config:
|
82
|
+
del self._global_config[setting]
|
83
|
+
config_changed = True
|
84
|
+
|
85
|
+
# Save changes if needed
|
86
|
+
if config_changed:
|
87
|
+
self._save_local_config()
|
88
|
+
self._save_global_config()
|
89
|
+
|
90
|
+
def _merge_configs(self) -> None:
|
91
|
+
"""Merge global and local configurations with local taking precedence."""
|
92
|
+
self._merged_config = merge_configs(self._global_config, self._local_config)
|
93
|
+
|
94
|
+
def _apply_config(self) -> None:
|
95
|
+
"""Apply the merged configuration to instance variables."""
|
96
|
+
config_data = self._merged_config
|
97
|
+
|
98
|
+
# Apply configuration values to instance variables
|
99
|
+
if "debug_mode" in config_data:
|
100
|
+
self._verbose = config_data["debug_mode"]
|
101
|
+
if "verbose" in config_data:
|
102
|
+
self._verbose = config_data["verbose"]
|
103
|
+
# ask_mode is a runtime-only setting, not loaded from config
|
104
|
+
if "trust_mode" in config_data:
|
105
|
+
self._trust_mode = config_data["trust_mode"]
|
106
|
+
if "show_usage_report" in config_data:
|
107
|
+
self._show_usage_report = config_data["show_usage_report"]
|
108
|
+
if "temperature" in config_data:
|
109
|
+
self._temperature = config_data["temperature"]
|
110
|
+
if "profile" in config_data:
|
111
|
+
self._profile = config_data["profile"]
|
112
|
+
if "role" in config_data:
|
113
|
+
self._role = config_data["role"]
|
114
|
+
if "gitbash_path" in config_data:
|
115
|
+
self._gitbash_path = config_data["gitbash_path"]
|
116
|
+
# max_view_lines is accessed directly from merged_config
|
117
|
+
|
118
|
+
def _save_local_config(self) -> None:
|
119
|
+
"""Save local configuration to file."""
|
120
|
+
config_path = get_local_config_path(self._workspace_dir)
|
121
|
+
save_config_file(config_path, self._local_config)
|
122
|
+
|
123
|
+
def _save_global_config(self) -> None:
|
124
|
+
"""Save global configuration to file."""
|
125
|
+
config_path = get_global_config_path()
|
126
|
+
save_config_file(config_path, self._global_config)
|
127
|
+
|
128
|
+
def _save_config(self) -> None:
|
129
|
+
"""Save local configuration to file (for backward compatibility)."""
|
130
|
+
self._save_local_config()
|
131
|
+
|
132
|
+
def set_profile(self, profile_name: str, config_type: str = "local") -> None:
|
133
|
+
"""
|
134
|
+
Set parameter values based on a predefined profile.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
profile_name: Name of the profile to use (precise, balanced, conversational, creative, technical)
|
138
|
+
config_type: Type of configuration to update ("local" or "global")
|
139
|
+
|
140
|
+
Raises:
|
141
|
+
ValueError: If the profile name is not recognized or config_type is invalid
|
142
|
+
"""
|
143
|
+
if config_type not in ["local", "global"]:
|
144
|
+
raise ValueError(f"Invalid config_type: {config_type}. Must be 'local' or 'global'")
|
145
|
+
|
146
|
+
profile = get_profile(profile_name)
|
147
|
+
|
148
|
+
# Update the appropriate configuration
|
149
|
+
if config_type == "local":
|
150
|
+
self.set_local_config("temperature", profile["temperature"])
|
151
|
+
self.set_local_config("profile", profile_name)
|
152
|
+
else:
|
153
|
+
self.set_global_config("temperature", profile["temperature"])
|
154
|
+
self.set_global_config("profile", profile_name)
|
155
|
+
|
156
|
+
@staticmethod
|
157
|
+
def get_available_profiles() -> Dict[str, Dict[str, Any]]:
|
158
|
+
"""Get all available predefined profiles."""
|
159
|
+
from ..profiles.manager import get_available_profiles
|
160
|
+
return get_available_profiles()
|
161
|
+
|
162
|
+
def set_local_config(self, key: str, value: Any) -> None:
|
163
|
+
"""
|
164
|
+
Set a configuration value in the local configuration.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
key: Configuration key
|
168
|
+
value: Configuration value
|
169
|
+
"""
|
170
|
+
self._local_config[key] = value
|
171
|
+
self._save_local_config()
|
172
|
+
|
173
|
+
# Re-merge and apply configurations
|
174
|
+
self._merge_configs()
|
175
|
+
self._apply_config()
|
176
|
+
|
177
|
+
def set_global_config(self, key: str, value: Any) -> None:
|
178
|
+
"""
|
179
|
+
Set a configuration value in the global configuration.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
key: Configuration key
|
183
|
+
value: Configuration value
|
184
|
+
"""
|
185
|
+
self._global_config[key] = value
|
186
|
+
self._save_global_config()
|
187
|
+
|
188
|
+
# Re-merge and apply configurations
|
189
|
+
self._merge_configs()
|
190
|
+
self._apply_config()
|
191
|
+
|
192
|
+
def get_local_config(self) -> Dict[str, Any]:
|
193
|
+
"""
|
194
|
+
Get the local configuration.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
Dict containing the local configuration
|
198
|
+
"""
|
199
|
+
return self._local_config.copy()
|
200
|
+
|
201
|
+
def get_global_config(self) -> Dict[str, Any]:
|
202
|
+
"""
|
203
|
+
Get the global configuration.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
Dict containing the global configuration
|
207
|
+
"""
|
208
|
+
return self._global_config.copy()
|
209
|
+
|
210
|
+
def get_merged_config(self) -> Dict[str, Any]:
|
211
|
+
"""
|
212
|
+
Get the merged configuration.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
Dict containing the merged configuration
|
216
|
+
"""
|
217
|
+
return self._merged_config.copy()
|
218
|
+
|
219
|
+
@staticmethod
|
220
|
+
def set_api_key(api_key: str) -> None:
|
221
|
+
"""
|
222
|
+
Set the API key in the global configuration file.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
api_key: The Anthropic API key to store
|
226
|
+
"""
|
227
|
+
# Get the singleton instance
|
228
|
+
config = Config()
|
229
|
+
|
230
|
+
# Set the API key in the global configuration
|
231
|
+
config.set_global_config("api_key", api_key)
|
232
|
+
print(f"API key saved to {get_global_config_path()}")
|
233
|
+
|
234
|
+
@staticmethod
|
235
|
+
def get_api_key() -> Optional[str]:
|
236
|
+
"""
|
237
|
+
Get the API key from the global configuration file.
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
The API key if found, None otherwise
|
241
|
+
"""
|
242
|
+
# Get the singleton instance
|
243
|
+
config = Config()
|
244
|
+
|
245
|
+
# Get the API key from the merged configuration
|
246
|
+
return config.get_merged_config().get("api_key")
|
247
|
+
|
248
|
+
def reset_local_config(self) -> bool:
|
249
|
+
"""
|
250
|
+
Reset local configuration by removing the local config file.
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
bool: True if the config file was removed, False if it didn't exist
|
254
|
+
"""
|
255
|
+
config_path = get_local_config_path(self._workspace_dir)
|
256
|
+
if config_path.exists():
|
257
|
+
config_path.unlink()
|
258
|
+
# Clear local configuration
|
259
|
+
self._local_config = {}
|
260
|
+
# Re-merge and apply configurations
|
261
|
+
self._merge_configs()
|
262
|
+
self._apply_config()
|
263
|
+
return True
|
264
|
+
return False
|
265
|
+
|
266
|
+
def reset_global_config(self) -> bool:
|
267
|
+
"""
|
268
|
+
Reset global configuration by removing the global config file.
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
bool: True if the config file was removed, False if it didn't exist
|
272
|
+
"""
|
273
|
+
config_path = get_global_config_path()
|
274
|
+
if config_path.exists():
|
275
|
+
config_path.unlink()
|
276
|
+
# Clear global configuration
|
277
|
+
self._global_config = {}
|
278
|
+
# Re-merge and apply configurations
|
279
|
+
self._merge_configs()
|
280
|
+
self._apply_config()
|
281
|
+
return True
|
282
|
+
return False
|
@@ -0,0 +1,8 @@
|
|
1
|
+
"""
|
2
|
+
Profile management for Janito configuration.
|
3
|
+
Provides predefined parameter profiles and related functionality.
|
4
|
+
"""
|
5
|
+
from .definitions import PROFILES
|
6
|
+
from .manager import get_profile, get_available_profiles
|
7
|
+
|
8
|
+
__all__ = ["PROFILES", "get_profile", "get_available_profiles"]
|
@@ -0,0 +1,38 @@
|
|
1
|
+
"""
|
2
|
+
Predefined parameter profiles for Janito.
|
3
|
+
"""
|
4
|
+
from typing import Dict, Any
|
5
|
+
|
6
|
+
# Predefined parameter profiles
|
7
|
+
PROFILES = {
|
8
|
+
"precise": {
|
9
|
+
"temperature": 0.2,
|
10
|
+
"top_p": 0.85,
|
11
|
+
"top_k": 20,
|
12
|
+
"description": "Factual answers, documentation, structured data, avoiding hallucinations"
|
13
|
+
},
|
14
|
+
"balanced": {
|
15
|
+
"temperature": 0.5,
|
16
|
+
"top_p": 0.9,
|
17
|
+
"top_k": 40,
|
18
|
+
"description": "Professional writing, summarization, everyday tasks with moderate creativity"
|
19
|
+
},
|
20
|
+
"conversational": {
|
21
|
+
"temperature": 0.7,
|
22
|
+
"top_p": 0.9,
|
23
|
+
"top_k": 45,
|
24
|
+
"description": "Natural dialogue, educational content, support conversations"
|
25
|
+
},
|
26
|
+
"creative": {
|
27
|
+
"temperature": 0.9,
|
28
|
+
"top_p": 0.95,
|
29
|
+
"top_k": 70,
|
30
|
+
"description": "Storytelling, brainstorming, marketing copy, poetry"
|
31
|
+
},
|
32
|
+
"technical": {
|
33
|
+
"temperature": 0.3,
|
34
|
+
"top_p": 0.95,
|
35
|
+
"top_k": 15,
|
36
|
+
"description": "Code generation, debugging, decision analysis, technical problem-solving"
|
37
|
+
}
|
38
|
+
}
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"""
|
2
|
+
Profile management functions for Janito configuration.
|
3
|
+
"""
|
4
|
+
from typing import Dict, Any
|
5
|
+
|
6
|
+
from .definitions import PROFILES
|
7
|
+
|
8
|
+
def get_available_profiles() -> Dict[str, Dict[str, Any]]:
|
9
|
+
"""
|
10
|
+
Get all available predefined profiles.
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
Dictionary of profile names to profile settings
|
14
|
+
"""
|
15
|
+
return PROFILES
|
16
|
+
|
17
|
+
def get_profile(profile_name: str) -> Dict[str, Any]:
|
18
|
+
"""
|
19
|
+
Get a specific profile by name.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
profile_name: Name of the profile to retrieve
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Dict containing the profile settings
|
26
|
+
|
27
|
+
Raises:
|
28
|
+
ValueError: If the profile name is not recognized
|
29
|
+
"""
|
30
|
+
profile_name = profile_name.lower()
|
31
|
+
if profile_name not in PROFILES:
|
32
|
+
valid_profiles = ", ".join(PROFILES.keys())
|
33
|
+
raise ValueError(f"Unknown profile: {profile_name}. Valid profiles are: {valid_profiles}")
|
34
|
+
|
35
|
+
return PROFILES[profile_name]
|
36
|
+
|
37
|
+
def create_custom_profile(name: str, temperature: float, description: str = None) -> Dict[str, Any]:
|
38
|
+
"""
|
39
|
+
Create a custom profile with the given parameters.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
name: Name for the custom profile
|
43
|
+
temperature: Temperature value (0.0 to 1.0)
|
44
|
+
description: Optional description for the profile
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Dict containing the profile settings
|
48
|
+
|
49
|
+
Raises:
|
50
|
+
ValueError: If temperature is not between 0.0 and 1.0
|
51
|
+
"""
|
52
|
+
if temperature < 0.0 or temperature > 1.0:
|
53
|
+
raise ValueError("Temperature must be between 0.0 and 1.0")
|
54
|
+
|
55
|
+
# Determine top_p and top_k based on temperature
|
56
|
+
if temperature <= 0.3:
|
57
|
+
top_p = 0.85
|
58
|
+
top_k = 15
|
59
|
+
elif temperature <= 0.6:
|
60
|
+
top_p = 0.9
|
61
|
+
top_k = 40
|
62
|
+
else:
|
63
|
+
top_p = 0.95
|
64
|
+
top_k = 60
|
65
|
+
|
66
|
+
# Use provided description or generate a default one
|
67
|
+
if description is None:
|
68
|
+
if temperature <= 0.3:
|
69
|
+
description = "Custom precise profile"
|
70
|
+
elif temperature <= 0.6:
|
71
|
+
description = "Custom balanced profile"
|
72
|
+
else:
|
73
|
+
description = "Custom creative profile"
|
74
|
+
|
75
|
+
return {
|
76
|
+
"temperature": temperature,
|
77
|
+
"top_p": top_p,
|
78
|
+
"top_k": top_k,
|
79
|
+
"description": description
|
80
|
+
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
You are a {{ role }}, using the name Janito .
|
2
2
|
You will be assisting an user using a computer system on a {{ platform }} platform.
|
3
3
|
You can find more about the current project using the tools in the workspace directory.
|
4
|
-
If the question is related to the project, use the tools using the relative path, filename instead of /filename.
|
4
|
+
If the question is related to the project, use the tools using the relative path, ./filename instead of /filename.
|
5
5
|
|
6
6
|
If creating or editing files with a large number of lines, organize them into smaller files.
|
7
7
|
If creating or editing files in an existing directory check surrounding files for the used patterns.
|
@@ -17,15 +17,18 @@ The bash tool does not support commands which will require user input.
|
|
17
17
|
Use the bash tool to get the current date or time when needed.
|
18
18
|
Prefer the str_replace_editor tool to view directories and file contents.
|
19
19
|
|
20
|
-
|
20
|
+
<IMPORTANT>
|
21
21
|
Call the user_prompt tool when:
|
22
22
|
- There are multiple options to apply a certain change
|
23
23
|
- The next operation risk is moderated or high
|
24
24
|
- The implementation plan is complex, requiring a review
|
25
25
|
Proceed according to the user answer.
|
26
|
-
|
26
|
+
</IMPORTANT>
|
27
27
|
|
28
28
|
When changing code in Python files, be mindful about the need to review the imports specially when new type hints are used (eg. Optional, Tuple, List, Dict, etc).
|
29
29
|
After performing changes to a project in interfaces which are exposed to the user, respond to the user with a short summary on how to verify the changes. eg. "run cmd xpto", prefer to provide a command to run instead of a description.
|
30
30
|
When displaying commands in instructions to the user, consider their platform.
|
31
31
|
When creating html pages which refer to images that should be manually placed by the user, instead of broken links provide a frame with a placeholder image.
|
32
|
+
|
33
|
+
If STRUCTURE.md was updated add it to the list of files to be committed.
|
34
|
+
After significant changes, run git commit with a message describing the changes made.
|
janito/tools/bash/bash.py
CHANGED
@@ -3,6 +3,9 @@ from typing import Tuple
|
|
3
3
|
import threading
|
4
4
|
import platform
|
5
5
|
import re
|
6
|
+
import queue
|
7
|
+
import signal
|
8
|
+
import time
|
6
9
|
from janito.config import get_config
|
7
10
|
from janito.tools.usage_tracker import get_tracker
|
8
11
|
from janito.tools.rich_console import console, print_info
|
@@ -16,6 +19,42 @@ else:
|
|
16
19
|
# Global instance of PersistentBash to maintain state between calls
|
17
20
|
_bash_session = None
|
18
21
|
_session_lock = threading.RLock() # Use RLock to allow reentrant locking
|
22
|
+
_current_bash_thread = None
|
23
|
+
_command_interrupted = False
|
24
|
+
|
25
|
+
def _execute_bash_command(command, result_queue):
|
26
|
+
"""
|
27
|
+
Execute a bash command in a separate thread.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
command: The bash command to execute
|
31
|
+
result_queue: Queue to store the result
|
32
|
+
"""
|
33
|
+
global _bash_session, _command_interrupted
|
34
|
+
|
35
|
+
try:
|
36
|
+
# Execute the command - output will be printed to console in real-time
|
37
|
+
output = _bash_session.execute(command)
|
38
|
+
|
39
|
+
# Put the result in the queue if the command wasn't interrupted
|
40
|
+
if not _command_interrupted:
|
41
|
+
result_queue.put((output, False))
|
42
|
+
except Exception as e:
|
43
|
+
# Handle any exceptions that might occur
|
44
|
+
error_message = f"Error executing bash command: {str(e)}"
|
45
|
+
console.print(error_message, style="red bold")
|
46
|
+
result_queue.put((error_message, True))
|
47
|
+
|
48
|
+
def _keyboard_interrupt_handler(signum, frame):
|
49
|
+
"""
|
50
|
+
Handle keyboard interrupt (Ctrl+C) by setting the interrupt flag.
|
51
|
+
"""
|
52
|
+
global _command_interrupted
|
53
|
+
_command_interrupted = True
|
54
|
+
console.print("\n[bold red]Command interrupted by user (Ctrl+C)[/bold red]")
|
55
|
+
|
56
|
+
# Restore the default signal handler
|
57
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
19
58
|
|
20
59
|
def bash_tool(command: str, restart: Optional[bool] = False) -> Tuple[str, bool]:
|
21
60
|
"""
|
@@ -23,6 +62,7 @@ def bash_tool(command: str, restart: Optional[bool] = False) -> Tuple[str, bool]
|
|
23
62
|
The appropriate implementation (Windows or Unix) is selected based on the detected platform.
|
24
63
|
When in ask mode, only read-only commands are allowed.
|
25
64
|
Output is printed to the console in real-time as it's received.
|
65
|
+
Command runs in a background thread, allowing Ctrl+C to interrupt just the command.
|
26
66
|
|
27
67
|
Args:
|
28
68
|
command: The bash command to execute
|
@@ -37,7 +77,9 @@ def bash_tool(command: str, restart: Optional[bool] = False) -> Tuple[str, bool]
|
|
37
77
|
# Only print command if not in trust mode
|
38
78
|
if not get_config().trust_mode:
|
39
79
|
print_info(f"{command}", "Bash Run")
|
40
|
-
|
80
|
+
|
81
|
+
global _bash_session, _current_bash_thread, _command_interrupted, original_sigint_handler
|
82
|
+
_command_interrupted = False
|
41
83
|
|
42
84
|
# Check if in ask mode and if the command might modify files
|
43
85
|
if get_config().ask_mode:
|
@@ -65,20 +107,51 @@ def bash_tool(command: str, restart: Optional[bool] = False) -> Tuple[str, bool]
|
|
65
107
|
_bash_session = PersistentBash(bash_path=gitbash_path)
|
66
108
|
|
67
109
|
try:
|
68
|
-
#
|
69
|
-
|
110
|
+
# Create a queue to get the result from the thread
|
111
|
+
result_queue = queue.Queue()
|
112
|
+
|
113
|
+
# Save the original SIGINT handler
|
114
|
+
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
115
|
+
|
116
|
+
# Set our custom SIGINT handler
|
117
|
+
signal.signal(signal.SIGINT, _keyboard_interrupt_handler)
|
118
|
+
|
119
|
+
# Create and start the thread
|
120
|
+
_current_bash_thread = threading.Thread(
|
121
|
+
target=_execute_bash_command,
|
122
|
+
args=(command, result_queue)
|
123
|
+
)
|
124
|
+
_current_bash_thread.daemon = True
|
125
|
+
_current_bash_thread.start()
|
126
|
+
|
127
|
+
# Wait for the thread to complete or for an interrupt
|
128
|
+
while _current_bash_thread.is_alive() and not _command_interrupted:
|
129
|
+
_current_bash_thread.join(0.1) # Check every 100ms
|
130
|
+
|
131
|
+
# If the command was interrupted, return a message
|
132
|
+
if _command_interrupted:
|
133
|
+
# Restore the original signal handler
|
134
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
135
|
+
return ("Command was interrupted by Ctrl+C", True)
|
136
|
+
|
137
|
+
# Get the result from the queue
|
138
|
+
output, is_error = result_queue.get(timeout=1)
|
139
|
+
|
140
|
+
# Restore the original signal handler
|
141
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
70
142
|
|
71
143
|
# Track bash command execution
|
72
144
|
get_tracker().increment('bash_commands')
|
73
145
|
|
74
|
-
#
|
75
|
-
is_error = False
|
76
|
-
|
77
|
-
# Return the output as a string (even though it was already printed in real-time)
|
146
|
+
# Return the output
|
78
147
|
return output, is_error
|
79
148
|
|
80
149
|
except Exception as e:
|
81
150
|
# Handle any exceptions that might occur
|
82
151
|
error_message = f"Error executing bash command: {str(e)}"
|
83
152
|
console.print(error_message, style="red bold")
|
153
|
+
|
154
|
+
# Restore the original signal handler
|
155
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
156
|
+
|
84
157
|
return error_message, True
|
@@ -132,7 +132,38 @@ class PersistentBash:
|
|
132
132
|
start_time = time.time()
|
133
133
|
max_wait = timeout if timeout is not None else 3600 # Default to 1 hour if no timeout
|
134
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
|
+
|
135
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
|
+
|
136
167
|
try:
|
137
168
|
line = self.process.stdout.readline().rstrip('\r\n')
|
138
169
|
if end_marker in line:
|
@@ -152,7 +183,7 @@ class PersistentBash:
|
|
152
183
|
continue
|
153
184
|
|
154
185
|
# Check for timeout
|
155
|
-
if time.time() - start_time >= max_wait + 5:
|
186
|
+
if time.time() - start_time >= max_wait + 5 and not _command_interrupted:
|
156
187
|
timeout_msg = f"Error: Command timed out after {max_wait} seconds"
|
157
188
|
console.print(timeout_msg, style="red bold")
|
158
189
|
output_lines.append(timeout_msg)
|
@@ -216,7 +216,40 @@ class PersistentBash:
|
|
216
216
|
start_time = time.time()
|
217
217
|
max_wait = timeout if timeout is not None else 3600 # Default to 1 hour if no timeout
|
218
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
|
+
|
219
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
|
+
|
220
253
|
try:
|
221
254
|
line = self.stdout.readline().rstrip('\r\n')
|
222
255
|
if end_marker in line:
|
@@ -243,7 +276,7 @@ class PersistentBash:
|
|
243
276
|
continue
|
244
277
|
|
245
278
|
# Check for timeout
|
246
|
-
if time.time() - start_time >= max_wait + 5:
|
279
|
+
if time.time() - start_time >= max_wait + 5 and not _command_interrupted:
|
247
280
|
timeout_msg = f"Error: Command timed out after {max_wait} seconds"
|
248
281
|
console.print(timeout_msg, style="red bold")
|
249
282
|
output_lines.append(timeout_msg)
|
janito/tools/move_file.py
CHANGED
@@ -64,7 +64,7 @@ def move_file(
|
|
64
64
|
try:
|
65
65
|
shutil.move(str(source_obj), str(destination_obj))
|
66
66
|
success_msg = f"Successfully moved file from {original_source} to {original_destination}"
|
67
|
-
print_success(
|
67
|
+
print_success("", "Success")
|
68
68
|
return (success_msg, False)
|
69
69
|
except Exception as e:
|
70
70
|
error_msg = f"Error moving file from {original_source} to {original_destination}: {str(e)}"
|