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