janito 0.11.0__py3-none-any.whl → 0.13.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/__main__.py +6 -204
- janito/callbacks.py +34 -132
- janito/cli/__init__.py +6 -0
- janito/cli/agent.py +400 -0
- janito/cli/app.py +94 -0
- janito/cli/commands.py +329 -0
- janito/cli/output.py +29 -0
- janito/cli/utils.py +22 -0
- janito/config.py +358 -121
- janito/data/instructions_template.txt +28 -0
- janito/token_report.py +154 -145
- janito/tools/__init__.py +38 -21
- janito/tools/bash/bash.py +84 -0
- janito/tools/bash/unix_persistent_bash.py +184 -0
- janito/tools/bash/win_persistent_bash.py +308 -0
- janito/tools/decorators.py +2 -13
- janito/tools/delete_file.py +27 -9
- janito/tools/fetch_webpage/__init__.py +34 -0
- janito/tools/fetch_webpage/chunking.py +76 -0
- janito/tools/fetch_webpage/core.py +155 -0
- janito/tools/fetch_webpage/extractors.py +276 -0
- janito/tools/fetch_webpage/news.py +137 -0
- janito/tools/fetch_webpage/utils.py +108 -0
- janito/tools/find_files.py +106 -44
- janito/tools/move_file.py +72 -0
- janito/tools/prompt_user.py +37 -6
- janito/tools/replace_file.py +31 -4
- janito/tools/rich_console.py +176 -0
- janito/tools/search_text.py +35 -22
- janito/tools/str_replace_editor/editor.py +7 -4
- janito/tools/str_replace_editor/handlers/__init__.py +16 -0
- janito/tools/str_replace_editor/handlers/create.py +60 -0
- janito/tools/str_replace_editor/handlers/insert.py +100 -0
- janito/tools/str_replace_editor/handlers/str_replace.py +94 -0
- janito/tools/str_replace_editor/handlers/undo.py +64 -0
- janito/tools/str_replace_editor/handlers/view.py +159 -0
- janito/tools/str_replace_editor/utils.py +0 -1
- janito/tools/usage_tracker.py +136 -0
- janito-0.13.0.dist-info/METADATA +300 -0
- janito-0.13.0.dist-info/RECORD +47 -0
- janito/chat_history.py +0 -117
- janito/data/instructions.txt +0 -4
- janito/tools/bash.py +0 -22
- janito/tools/str_replace_editor/handlers.py +0 -335
- janito-0.11.0.dist-info/METADATA +0 -86
- janito-0.11.0.dist-info/RECORD +0 -26
- {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/WHEEL +0 -0
- {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/entry_points.txt +0 -0
- {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/licenses/LICENSE +0 -0
janito/token_report.py
CHANGED
@@ -1,145 +1,154 @@
|
|
1
|
-
"""
|
2
|
-
Module for generating token usage reports.
|
3
|
-
"""
|
4
|
-
|
5
|
-
from rich.console import Console
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
Returns:
|
17
|
-
None - prints the report to the console
|
18
|
-
"""
|
19
|
-
console = Console()
|
20
|
-
usage = agent.get_tokens()
|
21
|
-
cost = agent.get_token_cost()
|
22
|
-
|
23
|
-
text_usage = usage.text_usage
|
24
|
-
tools_usage = usage.tools_usage
|
25
|
-
|
26
|
-
if verbose:
|
27
|
-
total_usage = usage.total_usage
|
28
|
-
|
29
|
-
# Get costs from the cost object
|
30
|
-
text_input_cost = cost.input_cost
|
31
|
-
text_output_cost = cost.output_cost
|
32
|
-
text_cache_creation_cost = cost.cache_creation_cost
|
33
|
-
text_cache_read_cost = cost.cache_read_cost
|
34
|
-
|
35
|
-
tools_input_cost = cost.input_cost
|
36
|
-
tools_output_cost = cost.output_cost
|
37
|
-
tools_cache_creation_cost = cost.cache_creation_cost
|
38
|
-
tools_cache_read_cost = cost.cache_read_cost
|
39
|
-
|
40
|
-
# Format costs
|
41
|
-
format_cost
|
42
|
-
|
43
|
-
|
44
|
-
console.print(
|
45
|
-
console.print(f"Text
|
46
|
-
console.print(f"Text
|
47
|
-
console.print(f"Text Cache
|
48
|
-
console.print(f"Text
|
49
|
-
|
50
|
-
|
51
|
-
console.print(f"Tool
|
52
|
-
console.print(f"Tool
|
53
|
-
console.print(f"Tool Cache
|
54
|
-
console.print(f"Tool
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
console.print(
|
60
|
-
console.print(f"Text
|
61
|
-
console.print(f"Text
|
62
|
-
console.print(f"Text Cache
|
63
|
-
console.print(f"Text
|
64
|
-
|
65
|
-
|
66
|
-
console.print(f"Tool
|
67
|
-
console.print(f"Tool
|
68
|
-
console.print(f"Tool Cache
|
69
|
-
console.print(f"Tool
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
console.print(f"
|
103
|
-
console.print(f"
|
104
|
-
console.print(f"
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
console.print(f"
|
117
|
-
console.print(f"
|
118
|
-
console.print(f"
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
1
|
+
"""
|
2
|
+
Module for generating token usage reports.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rich.console import Console
|
6
|
+
|
7
|
+
def generate_token_report(agent, verbose=False, interrupted=False):
|
8
|
+
"""
|
9
|
+
Generate a token usage report.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
agent: The Claude agent instance
|
13
|
+
verbose: Whether to show detailed token usage information
|
14
|
+
interrupted: Whether the request was interrupted
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
None - prints the report to the console
|
18
|
+
"""
|
19
|
+
console = Console()
|
20
|
+
usage = agent.get_tokens()
|
21
|
+
cost = agent.get_token_cost()
|
22
|
+
|
23
|
+
text_usage = usage.text_usage
|
24
|
+
tools_usage = usage.tools_usage
|
25
|
+
|
26
|
+
if verbose:
|
27
|
+
total_usage = usage.total_usage
|
28
|
+
|
29
|
+
# Get costs from the cost object
|
30
|
+
text_input_cost = cost.input_cost
|
31
|
+
text_output_cost = cost.output_cost
|
32
|
+
text_cache_creation_cost = cost.cache_creation_cost
|
33
|
+
text_cache_read_cost = cost.cache_read_cost
|
34
|
+
|
35
|
+
tools_input_cost = cost.input_cost
|
36
|
+
tools_output_cost = cost.output_cost
|
37
|
+
tools_cache_creation_cost = cost.cache_creation_cost
|
38
|
+
tools_cache_read_cost = cost.cache_read_cost
|
39
|
+
|
40
|
+
# Format costs
|
41
|
+
def format_cost(cost):
|
42
|
+
return f"{cost * 100:.2f}¢ USD" if cost < 1.0 else f"${cost:.6f} USD"
|
43
|
+
|
44
|
+
console.print("\n[bold blue]📊 Detailed Token Usage:[/bold blue]")
|
45
|
+
console.print(f"📝 Text Input tokens: {text_usage.input_tokens}")
|
46
|
+
console.print(f"📤 Text Output tokens: {text_usage.output_tokens}")
|
47
|
+
console.print(f"💾 Text Cache Creation tokens: {text_usage.cache_creation_input_tokens}")
|
48
|
+
console.print(f"📖 Text Cache Read tokens: {text_usage.cache_read_input_tokens}")
|
49
|
+
console.print(f"📋 Text Total tokens: {text_usage.input_tokens + text_usage.output_tokens + text_usage.cache_creation_input_tokens + text_usage.cache_read_input_tokens}")
|
50
|
+
|
51
|
+
console.print(f"🔧 Tool Input tokens: {tools_usage.input_tokens}")
|
52
|
+
console.print(f"🔨 Tool Output tokens: {tools_usage.output_tokens}")
|
53
|
+
console.print(f"💾 Tool Cache Creation tokens: {tools_usage.cache_creation_input_tokens}")
|
54
|
+
console.print(f"📖 Tool Cache Read tokens: {tools_usage.cache_read_input_tokens}")
|
55
|
+
console.print(f"🧰 Tool Total tokens: {tools_usage.input_tokens + tools_usage.output_tokens + tools_usage.cache_creation_input_tokens + tools_usage.cache_read_input_tokens}")
|
56
|
+
|
57
|
+
console.print(f"🔢 Total tokens: {total_usage.input_tokens + total_usage.output_tokens + total_usage.cache_creation_input_tokens + total_usage.cache_read_input_tokens}")
|
58
|
+
|
59
|
+
console.print("\n[bold blue]💰 Pricing Information:[/bold blue]")
|
60
|
+
console.print(f"📝 Text Input cost: {format_cost(text_input_cost)}")
|
61
|
+
console.print(f"📤 Text Output cost: {format_cost(text_output_cost)}")
|
62
|
+
console.print(f"💾 Text Cache Creation cost: {format_cost(text_cache_creation_cost)}")
|
63
|
+
console.print(f"📖 Text Cache Read cost: {format_cost(text_cache_read_cost)}")
|
64
|
+
console.print(f"📋 Text Total cost: {format_cost(text_input_cost + text_output_cost + text_cache_creation_cost + text_cache_read_cost)}")
|
65
|
+
|
66
|
+
console.print(f"🔧 Tool Input cost: {format_cost(tools_input_cost)}")
|
67
|
+
console.print(f"🔨 Tool Output cost: {format_cost(tools_output_cost)}")
|
68
|
+
console.print(f"💾 Tool Cache Creation cost: {format_cost(tools_cache_creation_cost)}")
|
69
|
+
console.print(f"📖 Tool Cache Read cost: {format_cost(tools_cache_read_cost)}")
|
70
|
+
console.print(f"🧰 Tool Total cost: {format_cost(tools_input_cost + tools_output_cost + tools_cache_creation_cost + tools_cache_read_cost)}")
|
71
|
+
|
72
|
+
total_cost_text = f"💵 Total cost: {format_cost(text_input_cost + text_output_cost + text_cache_creation_cost + text_cache_read_cost + tools_input_cost + tools_output_cost + tools_cache_creation_cost + tools_cache_read_cost)}"
|
73
|
+
if interrupted:
|
74
|
+
total_cost_text += " (interrupted request not accounted)"
|
75
|
+
console.print(total_cost_text)
|
76
|
+
|
77
|
+
# Show cache delta if available
|
78
|
+
if hasattr(cost, 'cache_delta') and cost.cache_delta:
|
79
|
+
cache_delta = cost.cache_delta
|
80
|
+
console.print(f"\n[bold green]💰 Cache Savings:[/bold green] {format_cost(cache_delta)}")
|
81
|
+
|
82
|
+
# Calculate percentage savings
|
83
|
+
total_cost_without_cache = cost.total_cost + cache_delta
|
84
|
+
if total_cost_without_cache > 0:
|
85
|
+
savings_percentage = (cache_delta / total_cost_without_cache) * 100
|
86
|
+
console.print(f"[bold green]📊 Cache Savings Percentage:[/bold green] {savings_percentage:.2f}%")
|
87
|
+
console.print(f"[bold green]💸 Cost without cache:[/bold green] {format_cost(total_cost_without_cache)}")
|
88
|
+
console.print(f"[bold green]💲 Cost with cache:[/bold green] {format_cost(cost.total_cost)}")
|
89
|
+
|
90
|
+
# Per-tool breakdown
|
91
|
+
if usage.by_tool:
|
92
|
+
console.print("\n[bold blue]🔧 Per-Tool Breakdown:[/bold blue]")
|
93
|
+
try:
|
94
|
+
if hasattr(cost, 'by_tool') and cost.by_tool:
|
95
|
+
for tool_name, tool_usage in usage.by_tool.items():
|
96
|
+
tool_input_cost = cost.by_tool[tool_name].input_cost
|
97
|
+
tool_output_cost = cost.by_tool[tool_name].output_cost
|
98
|
+
tool_cache_creation_cost = cost.by_tool[tool_name].cache_creation_cost
|
99
|
+
tool_cache_read_cost = cost.by_tool[tool_name].cache_read_cost
|
100
|
+
tool_total_cost = tool_input_cost + tool_output_cost + tool_cache_creation_cost + tool_cache_read_cost
|
101
|
+
|
102
|
+
console.print(f" 🔧 Tool: {tool_name}")
|
103
|
+
console.print(f" 📥 Input tokens: {tool_usage.input_tokens}")
|
104
|
+
console.print(f" 📤 Output tokens: {tool_usage.output_tokens}")
|
105
|
+
console.print(f" 💾 Cache Creation tokens: {tool_usage.cache_creation_input_tokens}")
|
106
|
+
console.print(f" 📖 Cache Read tokens: {tool_usage.cache_read_input_tokens}")
|
107
|
+
console.print(f" 🔢 Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens + tool_usage.cache_creation_input_tokens + tool_usage.cache_read_input_tokens}")
|
108
|
+
console.print(f" 💵 Total cost: {format_cost(tool_total_cost)}")
|
109
|
+
else:
|
110
|
+
# Calculate costs manually for each tool if cost.by_tool is not available
|
111
|
+
for tool_name, tool_usage in usage.by_tool.items():
|
112
|
+
# Estimate costs based on overall pricing
|
113
|
+
total_tokens = tool_usage.input_tokens + tool_usage.output_tokens + tool_usage.cache_creation_input_tokens + tool_usage.cache_read_input_tokens
|
114
|
+
estimated_cost = (total_tokens / (usage.total_usage.total_tokens + usage.total_usage.total_cache_tokens)) * cost.total_cost if usage.total_usage.total_tokens > 0 else 0
|
115
|
+
|
116
|
+
console.print(f" 🔧 Tool: {tool_name}")
|
117
|
+
console.print(f" 📥 Input tokens: {tool_usage.input_tokens}")
|
118
|
+
console.print(f" 📤 Output tokens: {tool_usage.output_tokens}")
|
119
|
+
console.print(f" 💾 Cache Creation tokens: {tool_usage.cache_creation_input_tokens}")
|
120
|
+
console.print(f" 📖 Cache Read tokens: {tool_usage.cache_read_input_tokens}")
|
121
|
+
console.print(f" 🔢 Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens + tool_usage.cache_creation_input_tokens + tool_usage.cache_read_input_tokens}")
|
122
|
+
console.print(f" 💵 Total cost: {format_cost(estimated_cost)}")
|
123
|
+
except Exception as e:
|
124
|
+
console.print(f"❌ Error: {str(e)}")
|
125
|
+
else:
|
126
|
+
total_tokens = (text_usage.input_tokens + text_usage.output_tokens +
|
127
|
+
text_usage.cache_creation_input_tokens + text_usage.cache_read_input_tokens +
|
128
|
+
tools_usage.input_tokens + tools_usage.output_tokens +
|
129
|
+
tools_usage.cache_creation_input_tokens + tools_usage.cache_read_input_tokens)
|
130
|
+
|
131
|
+
# Format costs
|
132
|
+
def format_cost(cost):
|
133
|
+
return f"{cost * 100:.2f}¢ USD" if cost < 1.0 else f"${cost:.6f} USD"
|
134
|
+
|
135
|
+
# Prepare summary message
|
136
|
+
cost_text = f"Cost: {format_cost(cost.total_cost)}"
|
137
|
+
if interrupted:
|
138
|
+
cost_text += " (interrupted request not accounted)"
|
139
|
+
|
140
|
+
summary = f"Total tokens: {total_tokens} | {cost_text}"
|
141
|
+
|
142
|
+
# Add cache savings if available
|
143
|
+
if hasattr(cost, 'cache_delta') and cost.cache_delta != 0:
|
144
|
+
cache_delta = cost.cache_delta
|
145
|
+
total_cost_without_cache = cost.total_cost + cache_delta
|
146
|
+
savings_percentage = 0
|
147
|
+
if total_cost_without_cache > 0:
|
148
|
+
savings_percentage = (cache_delta / total_cost_without_cache) * 100
|
149
|
+
|
150
|
+
summary += f" | Cache savings: {format_cost(cache_delta)} ({savings_percentage:.1f}%)"
|
151
|
+
|
152
|
+
# Display with a rule
|
153
|
+
console.rule("[blue]Token Usage[/blue]")
|
154
|
+
console.print(f"[blue]{summary}[/blue]", justify="center")
|
janito/tools/__init__.py
CHANGED
@@ -1,21 +1,38 @@
|
|
1
|
-
"""
|
2
|
-
Janito tools package.
|
3
|
-
"""
|
4
|
-
|
5
|
-
from .str_replace_editor import str_replace_editor
|
6
|
-
from .find_files import find_files
|
7
|
-
from .delete_file import delete_file
|
8
|
-
from .search_text import search_text
|
9
|
-
from .replace_file import replace_file
|
10
|
-
from .prompt_user import prompt_user
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
1
|
+
"""
|
2
|
+
Janito tools package.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .str_replace_editor import str_replace_editor
|
6
|
+
from .find_files import find_files
|
7
|
+
from .delete_file import delete_file
|
8
|
+
from .search_text import search_text
|
9
|
+
from .replace_file import replace_file
|
10
|
+
from .prompt_user import prompt_user
|
11
|
+
from .move_file import move_file
|
12
|
+
from janito.tools.fetch_webpage import fetch_webpage
|
13
|
+
from .usage_tracker import get_tracker, reset_tracker, print_usage_stats
|
14
|
+
from janito.config import get_config
|
15
|
+
|
16
|
+
__all__ = ["str_replace_editor", "find_files", "delete_file", "search_text", "replace_file",
|
17
|
+
"prompt_user", "move_file", "fetch_webpage", "get_tools",
|
18
|
+
"get_tracker", "reset_tracker", "print_usage_stats"]
|
19
|
+
|
20
|
+
def get_tools():
|
21
|
+
"""
|
22
|
+
Get a list of all available tools.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
List of tool functions (excluding str_replace_editor which is passed separately)
|
26
|
+
If ask_mode is enabled, only returns tools that don't perform changes
|
27
|
+
"""
|
28
|
+
# Tools that only read or view but don't modify anything
|
29
|
+
read_only_tools = [find_files, search_text, prompt_user, fetch_webpage]
|
30
|
+
|
31
|
+
# Tools that modify the filesystem
|
32
|
+
write_tools = [delete_file, replace_file, move_file]
|
33
|
+
|
34
|
+
# If ask_mode is enabled, only return read-only tools
|
35
|
+
if get_config().ask_mode:
|
36
|
+
return read_only_tools
|
37
|
+
else:
|
38
|
+
return read_only_tools + write_tools
|
@@ -0,0 +1,84 @@
|
|
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
|
@@ -0,0 +1,184 @@
|
|
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()
|