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
@@ -1,83 +1,102 @@
1
1
  import os
2
- import fnmatch
3
- from typing import List, Dict, Any, Tuple
4
- from janito.tools.decorators import tool_meta
2
+ import glob
3
+ import fnmatch # Still needed for gitignore pattern matching
4
+ from typing import List, Tuple
5
+ from janito.tools.rich_console import print_info, print_success, print_error, print_warning
5
6
 
6
7
 
7
- @tool_meta(label="Finding files matching path pattern {pattern}, on {root_dir} ({recursive and 'recursive' or 'non-recursive'}, {respect_gitignore and 'respecting gitignore' or 'ignoring gitignore'})")
8
- def find_files(pattern: str, root_dir: str = ".", recursive: bool = True, respect_gitignore: bool = True) -> Tuple[str, bool]:
8
+ def find_files(pattern: str, root_dir: str = ".", recursive: bool = True) -> Tuple[str, bool]:
9
9
  """
10
10
  Find files whose path matches a glob pattern.
11
+ Files in .gitignore are always ignored.
11
12
 
12
13
  Args:
13
14
  pattern: pattern to match file paths against (e.g., "*.py", "*/tools/*.py")
14
15
  root_dir: root directory to start search from (default: current directory)
15
16
  recursive: Whether to search recursively in subdirectories (default: True)
16
- respect_gitignore: Whether to respect .gitignore files (default: True)
17
17
 
18
18
  Returns:
19
19
  A tuple containing (message, is_error)
20
20
  """
21
+ # Print start message without newline
22
+ print_info(
23
+ f"Finding files matching path pattern {pattern}, on {root_dir} " +
24
+ f"({'recursive' if recursive else 'non-recursive'})",
25
+ title="Text Search"
26
+ )
21
27
  try:
22
28
  # Convert to absolute path if relative
23
29
  abs_root = os.path.abspath(root_dir)
24
30
 
25
31
  if not os.path.isdir(abs_root):
26
- return f"Error: Directory '{root_dir}' does not exist", True
32
+ error_msg = f"Error: Directory '{root_dir}' does not exist"
33
+ print_error(error_msg, title="File Operation")
34
+ return error_msg, True
27
35
 
28
36
  matching_files = []
29
37
 
30
- # Get gitignore patterns if needed
31
- ignored_patterns = []
32
- if respect_gitignore:
33
- ignored_patterns = _get_gitignore_patterns(abs_root)
38
+ # Get gitignore patterns
39
+ ignored_patterns = _get_gitignore_patterns(abs_root)
34
40
 
35
- # Use os.walk for more intuitive recursive behavior
41
+ # Check if the search pattern itself is in the gitignore
42
+ if _is_pattern_ignored(pattern, ignored_patterns):
43
+ warning_msg = f"Warning: The search pattern '{pattern}' matches patterns in .gitignore. Search may not yield expected results."
44
+ print_error(warning_msg, title="Text Search")
45
+ return warning_msg, True
46
+
47
+ # Use glob for pattern matching
48
+ # Construct the glob pattern with the root directory
49
+ glob_pattern = os.path.join(abs_root, pattern) if not pattern.startswith(os.path.sep) else pattern
50
+
51
+ # Use recursive glob if needed
36
52
  if recursive:
37
- for dirpath, dirnames, filenames in os.walk(abs_root):
38
- # Skip ignored directories
39
- if respect_gitignore:
40
- dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
41
-
42
- for filename in filenames:
43
- file_path = os.path.join(dirpath, filename)
44
-
45
- # Skip ignored files
46
- if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
47
- continue
48
-
49
- # Convert to relative path from root_dir
50
- rel_path = os.path.relpath(file_path, abs_root)
51
- # Match against the relative path, not just the filename
52
- if fnmatch.fnmatch(rel_path, pattern):
53
- matching_files.append(rel_path)
53
+ # Use ** pattern for recursive search if not already in the pattern
54
+ if '**' not in glob_pattern:
55
+ # Check if the pattern already has a directory component
56
+ if os.path.sep in pattern or '/' in pattern:
57
+ # Pattern already has directory component, keep as is
58
+ pass
59
+ else:
60
+ # Add ** to search in all subdirectories
61
+ glob_pattern = os.path.join(abs_root, '**', pattern)
62
+
63
+ # Use recursive=True for Python 3.5+ glob
64
+ glob_files = glob.glob(glob_pattern, recursive=True)
54
65
  else:
55
66
  # Non-recursive mode - only search in the specified directory
56
- for filename in os.listdir(abs_root):
57
- file_path = os.path.join(abs_root, filename)
58
-
59
- # Skip ignored files
60
- if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
61
- continue
67
+ glob_files = glob.glob(glob_pattern)
68
+
69
+ # Process the glob results
70
+ for file_path in glob_files:
71
+ # Skip directories
72
+ if not os.path.isfile(file_path):
73
+ continue
62
74
 
63
- if os.path.isfile(file_path):
64
- # Convert to relative path from root_dir
65
- rel_path = os.path.relpath(file_path, abs_root)
66
- # Match against the relative path, not just the filename
67
- if fnmatch.fnmatch(rel_path, pattern):
68
- matching_files.append(rel_path)
75
+ # Skip ignored files
76
+ if _is_ignored(file_path, ignored_patterns, abs_root):
77
+ continue
78
+
79
+ # Convert to relative path from root_dir
80
+ rel_path = os.path.relpath(file_path, abs_root)
81
+ matching_files.append(rel_path)
69
82
 
70
83
  # Sort the files for consistent output
71
84
  matching_files.sort()
72
85
 
73
86
  if matching_files:
74
87
  file_list = "\n- ".join(matching_files)
75
- return f"Found {len(matching_files)} files matching pattern '{pattern}':\n- {file_list}\n{len(matching_files)}", False
88
+ result_msg = f"{len(matching_files)} files found"
89
+ print_success(result_msg, title="Search Results")
90
+ return file_list, False
76
91
  else:
77
- return f"No files found matching pattern '{pattern}' in '{root_dir}'", False
92
+ result_msg = "No files found"
93
+ print_success(result_msg, title="Search Results")
94
+ return result_msg, False
78
95
 
79
96
  except Exception as e:
80
- return f"Error finding files: {str(e)}", True
97
+ error_msg = f"Error finding files: {str(e)}"
98
+ print_error(error_msg, title="Text Search")
99
+ return error_msg, True
81
100
 
82
101
 
83
102
  def _get_gitignore_patterns(root_dir: str) -> List[str]:
@@ -115,6 +134,49 @@ def _get_gitignore_patterns(root_dir: str) -> List[str]:
115
134
  return patterns
116
135
 
117
136
 
137
+ def _is_pattern_ignored(search_pattern: str, gitignore_patterns: List[str]) -> bool:
138
+ """
139
+ Check if a search pattern conflicts with gitignore patterns.
140
+
141
+ Args:
142
+ search_pattern: The search pattern to check
143
+ gitignore_patterns: List of gitignore patterns
144
+
145
+ Returns:
146
+ True if the search pattern conflicts with gitignore patterns, False otherwise
147
+ """
148
+ # Remove any directory part from the search pattern
149
+ pattern_only = search_pattern.split('/')[-1]
150
+
151
+ for git_pattern in gitignore_patterns:
152
+ # Skip negation patterns
153
+ if git_pattern.startswith('!'):
154
+ continue
155
+
156
+ # Remove trailing slash for directory patterns
157
+ if git_pattern.endswith('/'):
158
+ git_pattern = git_pattern[:-1]
159
+
160
+ # Direct match
161
+ if git_pattern == search_pattern or git_pattern == pattern_only:
162
+ return True
163
+
164
+ # Check if the gitignore pattern is a prefix of the search pattern
165
+ if search_pattern.startswith(git_pattern) and (
166
+ len(git_pattern) == len(search_pattern) or
167
+ search_pattern[len(git_pattern)] in ['/', '\\']
168
+ ):
169
+ return True
170
+
171
+ # Check for wildcard matches
172
+ if '*' in git_pattern or '?' in git_pattern:
173
+ # Check if the search pattern would be caught by this gitignore pattern
174
+ if fnmatch.fnmatch(search_pattern, git_pattern) or fnmatch.fnmatch(pattern_only, git_pattern):
175
+ return True
176
+
177
+ return False
178
+
179
+
118
180
  def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
119
181
  """
120
182
  Check if a path should be ignored based on gitignore patterns.
@@ -0,0 +1,72 @@
1
+ """
2
+ Tool for moving files through the claudine agent.
3
+ """
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Tuple
7
+ from janito.tools.str_replace_editor.utils import normalize_path
8
+ from janito.tools.rich_console import print_info, print_success, print_error
9
+ from janito.tools.usage_tracker import track_usage
10
+
11
+
12
+ @track_usage('files_moved')
13
+ def move_file(
14
+ source_path: str,
15
+ destination_path: str,
16
+ ) -> Tuple[str, bool]:
17
+ """
18
+ Move a file from source path to destination path.
19
+
20
+ Args:
21
+ source_path: Path to the file to move, relative to the workspace directory
22
+ destination_path: Destination path where the file should be moved, relative to the workspace directory
23
+
24
+ Returns:
25
+ A tuple containing (message, is_error)
26
+ """
27
+ print_info(f"Moving file from {source_path} to {destination_path}", "Move Operation")
28
+
29
+ # Store the original paths for display purposes
30
+ original_source = source_path
31
+ original_destination = destination_path
32
+
33
+ # Normalize the file paths (converts to absolute paths)
34
+ source = normalize_path(source_path)
35
+ destination = normalize_path(destination_path)
36
+
37
+ # Convert to Path objects for better path handling
38
+ source_obj = Path(source)
39
+ destination_obj = Path(destination)
40
+
41
+ # Check if the source file exists
42
+ if not source_obj.exists():
43
+ error_msg = f"Source file {original_source} does not exist."
44
+ print_error(error_msg, "Error")
45
+ return (error_msg, True)
46
+
47
+ # Check if source is a directory
48
+ if source_obj.is_dir():
49
+ error_msg = f"{original_source} is a directory, not a file. Use move_directory for directories."
50
+ print_error(error_msg, "Error")
51
+ return (error_msg, True)
52
+
53
+ # Check if destination directory exists
54
+ if not destination_obj.parent.exists():
55
+ try:
56
+ destination_obj.parent.mkdir(parents=True, exist_ok=True)
57
+ print_info(f"Created directory: {destination_obj.parent}", "Info")
58
+ except Exception as e:
59
+ error_msg = f"Error creating destination directory: {str(e)}"
60
+ print_error(error_msg, "Error")
61
+ return (error_msg, True)
62
+
63
+ # Move the file
64
+ try:
65
+ shutil.move(str(source_obj), str(destination_obj))
66
+ success_msg = f"Successfully moved file from {original_source} to {original_destination}"
67
+ print_success(success_msg, "Success")
68
+ return (success_msg, False)
69
+ except Exception as e:
70
+ error_msg = f"Error moving file from {original_source} to {original_destination}: {str(e)}"
71
+ print_error(error_msg, "Error")
72
+ return (error_msg, True)
@@ -1,16 +1,24 @@
1
1
  """
2
2
  Tool for prompting the user for input through the claudine agent.
3
3
  """
4
- from typing import Tuple
5
- from janito.tools.decorators import tool_meta
4
+ from typing import Tuple, List
5
+ import sys
6
+ import textwrap
7
+ from rich.console import Console
8
+ from janito.tools.rich_console import print_info, print_error, print_warning
9
+ from janito.tools.usage_tracker import track_usage
10
+ from janito.cli.utils import get_stdin_termination_hint
6
11
 
7
12
 
8
- @tool_meta(label="Prompting user with '{prompt_text}'")
13
+ console = Console()
14
+
15
+ @track_usage('user_prompts')
9
16
  def prompt_user(
10
17
  prompt_text: str,
11
18
  ) -> Tuple[str, bool]:
12
19
  """
13
20
  Prompt the user for input and return their response.
21
+ Displays the prompt in a panel and uses stdin for input.
14
22
 
15
23
  Args:
16
24
  prompt_text: Text to display to the user as a prompt
@@ -19,8 +27,31 @@ def prompt_user(
19
27
  A tuple containing (user_response, is_error)
20
28
  """
21
29
  try:
22
- # Print the prompt and get user input
23
- user_response = input(f"{prompt_text}")
30
+ # Display the prompt with ASCII header
31
+ console.print("\n" + "="*50)
32
+ console.print("USER PROMPT")
33
+ console.print("="*50)
34
+ console.print(prompt_text)
35
+
36
+ # Show input instructions with stdin termination hint
37
+ termination_hint = get_stdin_termination_hint().replace("[bold yellow]", "").replace("[/bold yellow]", "")
38
+ print_info(f"Enter your response below. {termination_hint}\n", "Input Instructions")
39
+
40
+ # Read input from stdin
41
+ lines = []
42
+ for line in sys.stdin:
43
+ lines.append(line.rstrip('\n'))
44
+
45
+ # Join the lines with newlines to preserve the multiline format
46
+ user_response = "\n".join(lines)
47
+
48
+ # If no input was provided, return a message
49
+ if not user_response.strip():
50
+ print_warning("No input was provided. Empty Input.")
51
+ return ("", False)
52
+
24
53
  return (user_response, False)
25
54
  except Exception as e:
26
- return (f"Error prompting user: {str(e)}", True)
55
+ error_msg = f"Error prompting user: {str(e)}"
56
+ print_error(error_msg, "Prompt Error")
57
+ return (error_msg, True)
@@ -5,9 +5,12 @@ import os
5
5
  from typing import Tuple
6
6
 
7
7
  from janito.tools.decorators import tool
8
+ from janito.tools.rich_console import print_info, print_success, print_error
9
+ from janito.tools.usage_tracker import track_usage, get_tracker
8
10
 
9
11
 
10
12
  @tool
13
+ @track_usage('files_modified')
11
14
  def replace_file(file_path: str, new_content: str) -> Tuple[str, bool]:
12
15
  """
13
16
  Replace an existing file with new content.
@@ -20,17 +23,41 @@ def replace_file(file_path: str, new_content: str) -> Tuple[str, bool]:
20
23
  A tuple containing (message, is_error)
21
24
  """
22
25
  try:
26
+ print_info(f"Replacing file '{file_path}'", "File Operation")
27
+
23
28
  # Convert relative path to absolute path
24
29
  abs_path = os.path.abspath(file_path)
25
30
 
26
31
  # Check if file exists
27
32
  if not os.path.isfile(abs_path):
28
- return f"Error: File '{file_path}' does not exist", True
33
+ error_msg = f"Error: File '{file_path}' does not exist"
34
+ print_error(error_msg, "File Error")
35
+ return error_msg, True
36
+
37
+ # Read the original content to calculate line delta
38
+ try:
39
+ with open(abs_path, 'r', encoding='utf-8') as f:
40
+ old_content = f.read()
41
+
42
+ # Calculate line delta
43
+ old_lines_count = len(old_content.splitlines()) if old_content else 0
44
+ new_lines_count = len(new_content.splitlines()) if new_content else 0
45
+ line_delta = new_lines_count - old_lines_count
46
+
47
+ # Track line delta
48
+ get_tracker().increment('lines_delta', line_delta)
49
+ except Exception:
50
+ # If we can't read the file, we can't calculate line delta
51
+ pass
29
52
 
30
53
  # Write new content to the file
31
54
  with open(abs_path, 'w', encoding='utf-8') as f:
32
55
  f.write(new_content)
33
-
34
- return f"Successfully replaced file '{file_path}'", False
56
+
57
+ success_msg = f"Successfully replaced file '{file_path}'"
58
+ print_success(success_msg, "Success")
59
+ return success_msg, False
35
60
  except Exception as e:
36
- return f"Error replacing file '{file_path}': {str(e)}", True
61
+ error_msg = f"Error replacing file '{file_path}': {str(e)}"
62
+ print_error(error_msg, "Error")
63
+ return error_msg, True
@@ -0,0 +1,176 @@
1
+ """
2
+ Utility module for rich console printing in tools.
3
+ """
4
+ from rich.console import Console
5
+ from rich.text import Text
6
+ from typing import Optional
7
+ from janito.config import get_config
8
+
9
+ # Create a shared console instance
10
+ console = Console()
11
+
12
+ def print_info(message: str, title: Optional[str] = None):
13
+ """
14
+ Print an informational message with rich formatting.
15
+
16
+ Args:
17
+ message: The message to print
18
+ title: Optional title for the panel
19
+ """
20
+ # Skip printing if trust mode is enabled
21
+ if get_config().trust_mode:
22
+ return
23
+ # Map titles to specific icons
24
+ icon_map = {
25
+ # File operations
26
+ "Delete Operation": "🗑️ ",
27
+ "Move Operation": "📦",
28
+ "File Operation": "📄",
29
+ "Directory View": "📁",
30
+ "File View": "📄",
31
+ "File Creation": "📝",
32
+ "Undo Operation": "↩️",
33
+
34
+ # Search and find operations
35
+ "Text Search": "🔍",
36
+ "Search Results": "📊",
37
+
38
+ # Web operations
39
+ "Web Fetch": "🌐",
40
+ "Content Extraction": "📰",
41
+ "News Extraction": "📰",
42
+ "Targeted Extraction": "🎯",
43
+ "Content Chunking": "📊",
44
+ "Content Ex": "📰", # For truncated "Content Extraction" in search results
45
+
46
+ # Command execution
47
+ "Bash Run": "🔄",
48
+
49
+ # User interaction
50
+ "Input Instructions": "⌨️",
51
+
52
+ # Default
53
+ "Info": "ℹ️ ",
54
+ }
55
+
56
+ # Get the appropriate icon based on title and message content
57
+ icon = "ℹ️ " # Default icon
58
+
59
+ # Check for exact matches in the icon map based on title
60
+ if title and title in icon_map:
61
+ icon = icon_map[title]
62
+ else:
63
+ # Check for matching strings in both title and message
64
+ for key, value in icon_map.items():
65
+ # Skip the default "Info" key to avoid too many matches
66
+ if key == "Info":
67
+ continue
68
+
69
+ # Check if the key appears in both title and message (if title exists)
70
+ if title and key in title and key in message:
71
+ icon = value
72
+ break
73
+
74
+ # If no match found yet, check for partial matches for str_replace_editor operations
75
+ if title:
76
+ if "Replacing text in file" in title:
77
+ icon = "✏️ " # Edit icon
78
+ elif "Inserting text in file" in title:
79
+ icon = "➕" # Plus icon
80
+ elif "Viewing file" in title:
81
+ icon = "📄" # File icon
82
+ elif "Viewing directory" in title:
83
+ icon = "📁" # Directory icon
84
+ elif "Creating file" in title:
85
+ icon = "📝" # Create icon
86
+ elif "Undoing last edit" in title:
87
+ icon = "↩️" # Undo icon
88
+
89
+ # Add indentation to all tool messages
90
+ indent = " "
91
+ text = Text(message)
92
+ if title:
93
+ # Special case for Bash Run commands
94
+ if title == "Bash Run":
95
+ console.print("\n" + "-"*50)
96
+ console.print(f"{indent}{icon} {title}", style="bold white on blue")
97
+ console.print("-"*50)
98
+ console.print(f"{indent}$ {text}", style="white on dark_blue")
99
+ # Make sure we're not returning anything
100
+ return
101
+ else:
102
+ console.print(f"{indent}{icon} {message}", style="blue", end="")
103
+ else:
104
+ console.print(f"{indent}{icon} {text}", style="blue", end="")
105
+
106
+ def print_success(message: str, title: Optional[str] = None):
107
+ """
108
+ Print a success message with rich formatting.
109
+
110
+ Args:
111
+ message: The message to print
112
+ title: Optional title for the panel
113
+ """
114
+ # Skip printing if trust mode is enabled
115
+ if get_config().trust_mode:
116
+ return
117
+ text = Text(message)
118
+ if title:
119
+ console.print(f" ✅ {message}", style="green")
120
+ else:
121
+ console.print(f"✅ {text}", style="green")
122
+
123
+ def print_error(message: str, title: Optional[str] = None):
124
+ """
125
+ Print an error message with rich formatting.
126
+ In trust mode, error messages are suppressed.
127
+
128
+ Args:
129
+ message: The message to print
130
+ title: Optional title for the panel
131
+ """
132
+ # Skip printing if trust mode is enabled
133
+ if get_config().trust_mode:
134
+ return
135
+
136
+ text = Text(message)
137
+
138
+ # Check if message starts with question mark emoji (❓)
139
+ # If it does, use warning styling (yellow) instead of error styling (red)
140
+ starts_with_question_mark = message.startswith("❓")
141
+
142
+ if starts_with_question_mark:
143
+ # Use warning styling for question mark emoji errors
144
+ # For question mark emoji errors, don't include the title (like "Error")
145
+ # Just print the message with the emoji
146
+ if title == "File View":
147
+ console.print(f"\n {message}", style="yellow")
148
+ else:
149
+ console.print(f"{message}", style="yellow")
150
+ else:
151
+ # Regular error styling
152
+ if title:
153
+ # Special case for File View - print without header
154
+ if title == "File View":
155
+ console.print(f"\n ❌ {message}", style="red")
156
+ # Special case for Search Error
157
+ elif title == "Search Error":
158
+ console.print(f"❌ {message}", style="red")
159
+ else:
160
+ console.print(f"❌ {title} {text}", style="red")
161
+ else:
162
+ console.print(f"\n❌ {text}", style="red")
163
+
164
+ def print_warning(message: str):
165
+ """
166
+ Print a warning message with rich formatting.
167
+ In trust mode, warning messages are suppressed.
168
+
169
+ Args:
170
+ message: The message to print
171
+ """
172
+ # Skip printing if trust mode is enabled
173
+ if get_config().trust_mode:
174
+ return
175
+
176
+ console.print(f"⚠️ {message}", style="yellow")