codemate-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,277 @@
1
+ # codemate/utils/error_handler.py
2
+ """Enhanced error handling for CodeMate CLI"""
3
+
4
+ import re
5
+ import webbrowser
6
+ from typing import Optional, Tuple
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+ from rich import box
11
+
12
+
13
+ class ErrorHandler:
14
+ """Centralized error handling for CodeMate CLI"""
15
+
16
+ # Upgrade URL for plan limits
17
+ UPGRADE_URL = "https://app.codemate.ai/settings"
18
+
19
+ def __init__(self, console: Console):
20
+ self.console = console
21
+
22
+ def parse_error_message(self, error_msg: str) -> Tuple[str, str, Optional[str]]:
23
+ """
24
+ Parse error message and categorize it
25
+
26
+ Returns:
27
+ Tuple of (error_type, display_message, upgrade_url)
28
+ - error_type: "connection", "rate_limit", "server", "generic"
29
+ - display_message: User-friendly message
30
+ - upgrade_url: URL for upgrade (if rate limit error)
31
+ """
32
+ error_lower = error_msg.lower()
33
+
34
+ # Connection errors
35
+ if "connection" in error_lower or "failed to connect" in error_lower:
36
+ return (
37
+ "connection",
38
+ "Unable to connect to CodeMate server. Please check:\n"
39
+ " • Is the server running?\n"
40
+ " • Check your internet connection\n"
41
+ " • Verify server endpoint in configuration",
42
+ None
43
+ )
44
+
45
+ # All connection attempts failed
46
+ if "all connection attempts failed" in error_lower:
47
+ return (
48
+ "connection",
49
+ "Cannot reach CodeMate server. Please ensure:\n"
50
+ " • The server is running and accessible\n"
51
+ " • Your network connection is stable\n"
52
+ " • Firewall is not blocking the connection",
53
+ None
54
+ )
55
+
56
+ # Rate limit errors - parse for details
57
+ rate_limit_match = re.search(
58
+ r'rate limit exceeded.*?you have used (\d+)/(\d+) requests',
59
+ error_lower
60
+ )
61
+ if rate_limit_match or "rate limit" in error_lower or "ratelimiterror" in error_lower:
62
+ used = rate_limit_match.group(1) if rate_limit_match else "N/A"
63
+ limit = rate_limit_match.group(2) if rate_limit_match else "N/A"
64
+
65
+ return (
66
+ "rate_limit",
67
+ f"Rate Limit Exceeded\n\n"
68
+ f"You have used {used}/{limit} requests.\n"
69
+ f"Please upgrade your plan for higher limits.\n\n"
70
+ f"Upgrade URL: {self.UPGRADE_URL}",
71
+ self.UPGRADE_URL
72
+ )
73
+
74
+ # 429 Status Code (Too Many Requests)
75
+ if "429" in error_msg or "too many requests" in error_lower:
76
+ return (
77
+ "rate_limit",
78
+ f"Too Many Requests (429)\n\n"
79
+ f"You've exceeded your request limit.\n"
80
+ f"Please upgrade your plan for higher limits.\n\n"
81
+ f"Upgrade URL: {self.UPGRADE_URL}",
82
+ self.UPGRADE_URL
83
+ )
84
+
85
+ # Server errors (5xx)
86
+ if any(code in error_msg for code in ["500", "502", "503", "504"]):
87
+ return (
88
+ "server",
89
+ "Internal Server Error\n\n"
90
+ "The CodeMate service is temporarily unavailable.\n"
91
+ "Please try again in a few moments.",
92
+ None
93
+ )
94
+
95
+ # Authentication errors
96
+ if "unauthorized" in error_lower or "401" in error_msg:
97
+ return (
98
+ "auth",
99
+ "Authentication Failed\n\n"
100
+ "Your session may have expired.\n"
101
+ "Please login again using /logout and restart CLI.",
102
+ None
103
+ )
104
+
105
+ # Timeout errors
106
+ if "timeout" in error_lower or "timed out" in error_lower:
107
+ return (
108
+ "timeout",
109
+ "Request Timeout\n\n"
110
+ "The server took too long to respond.\n"
111
+ "Please try again or check your connection.",
112
+ None
113
+ )
114
+
115
+ # Generic error
116
+ return (
117
+ "generic",
118
+ f"An error occurred:\n{error_msg}",
119
+ None
120
+ )
121
+
122
+ def display_connection_error(self):
123
+ """Display connection error panel"""
124
+ self.console.print()
125
+ self.console.print(Panel(
126
+ "[red]⚠️ Cannot Connect to Server[/red]\n\n"
127
+ "[white]Please verify:[/white]\n"
128
+ " [cyan]•[/cyan] Is the CodeMate server running?\n"
129
+ " [cyan]•[/cyan] Check your internet connection\n"
130
+ " [cyan]•[/cyan] Verify server endpoint configuration\n\n"
131
+ "[dim]Run [cyan]codemate-cli --help[/cyan] for more information[/dim]",
132
+ border_style="red",
133
+ title="[bold red]Connection Error[/bold red]",
134
+ padding=(1, 2),
135
+ box=box.ROUNDED
136
+ ))
137
+ self.console.print()
138
+
139
+ def display_rate_limit_error(self, used: str = "N/A", limit: str = "N/A"):
140
+ """Display rate limit error with upgrade prompt"""
141
+ self.console.print()
142
+ self.console.print(Panel(
143
+ f"[yellow]⚠️ Rate Limit Exceeded[/yellow]\n\n"
144
+ f"[white]Usage:[/white] {used}/{limit} requests\n\n"
145
+ f"[white]You've reached your plan's request limit.[/white]\n"
146
+ f"[cyan]Upgrade your plan for higher limits.[/cyan]\n\n"
147
+ f"[dim]Upgrade URL:[/dim] [link={self.UPGRADE_URL}]{self.UPGRADE_URL}[/link]",
148
+ border_style="yellow",
149
+ title="[bold yellow]⚡ Upgrade Required[/bold yellow]",
150
+ padding=(1, 2),
151
+ box=box.ROUNDED
152
+ ))
153
+
154
+ # Prompt user to open browser
155
+ self.console.print()
156
+ try:
157
+ response = self.console.input(
158
+ "[cyan]Would you like to open the upgrade page in your browser? (y/n):[/cyan] "
159
+ ).strip().lower()
160
+
161
+ if response in ['y', 'yes']:
162
+ try:
163
+ webbrowser.open(self.UPGRADE_URL)
164
+ self.console.print("[green]✓[/green] Opening browser...")
165
+ except Exception as e:
166
+ self.console.print(f"[yellow]Could not open browser: {e}[/yellow]")
167
+ self.console.print(f"[dim]Please visit: {self.UPGRADE_URL}[/dim]")
168
+ except (KeyboardInterrupt, EOFError):
169
+ self.console.print()
170
+
171
+ self.console.print()
172
+
173
+ def display_server_error(self):
174
+ """Display server error panel"""
175
+ self.console.print()
176
+ self.console.print(Panel(
177
+ "[red]⚠️ Internal Server Error[/red]\n\n"
178
+ "[white]The CodeMate service is temporarily unavailable.[/white]\n\n"
179
+ "[dim]Please try again in a few moments.\n"
180
+ "If the issue persists, contact support.[/dim]",
181
+ border_style="red",
182
+ title="[bold red]Service Unavailable[/bold red]",
183
+ padding=(1, 2),
184
+ box=box.ROUNDED
185
+ ))
186
+ self.console.print()
187
+
188
+ def display_auth_error(self):
189
+ """Display authentication error panel"""
190
+ self.console.print()
191
+ self.console.print(Panel(
192
+ "[red]⚠️ Authentication Failed[/red]\n\n"
193
+ "[white]Your session may have expired.[/white]\n\n"
194
+ "[cyan]Please run:[/cyan] [bold]/logout[/bold]\n"
195
+ "[dim]Then restart the CLI to login again.[/dim]",
196
+ border_style="red",
197
+ title="[bold red]Session Expired[/bold red]",
198
+ padding=(1, 2),
199
+ box=box.ROUNDED
200
+ ))
201
+ self.console.print()
202
+
203
+ def display_timeout_error(self):
204
+ """Display timeout error panel"""
205
+ self.console.print()
206
+ self.console.print(Panel(
207
+ "[yellow]⚠️ Request Timeout[/yellow]\n\n"
208
+ "[white]The server took too long to respond.[/white]\n\n"
209
+ "[dim]Please try again or check your connection.[/dim]",
210
+ border_style="yellow",
211
+ title="[bold yellow]Timeout[/bold yellow]",
212
+ padding=(1, 2),
213
+ box=box.ROUNDED
214
+ ))
215
+ self.console.print()
216
+
217
+ def display_generic_error(self, error_msg: str):
218
+ """Display generic error panel"""
219
+ self.console.print()
220
+ self.console.print(Panel(
221
+ f"[red]❌ Error[/red]\n\n"
222
+ f"[white]{error_msg}[/white]\n\n"
223
+ f"[dim]If this issue persists, please contact support.[/dim]",
224
+ border_style="red",
225
+ title="[bold red]Error[/bold red]",
226
+ padding=(1, 2),
227
+ box=box.ROUNDED
228
+ ))
229
+ self.console.print()
230
+
231
+ def handle_error(self, error: Exception):
232
+ """Main error handler - routes to appropriate display method"""
233
+ error_msg = str(error)
234
+ error_type, display_msg, upgrade_url = self.parse_error_message(error_msg)
235
+
236
+ if error_type == "connection":
237
+ self.display_connection_error()
238
+ elif error_type == "rate_limit":
239
+ # Extract usage numbers if available
240
+ match = re.search(r'(\d+)/(\d+) requests', error_msg)
241
+ if match:
242
+ self.display_rate_limit_error(match.group(1), match.group(2))
243
+ else:
244
+ self.display_rate_limit_error()
245
+ elif error_type == "server":
246
+ self.display_server_error()
247
+ elif error_type == "auth":
248
+ self.display_auth_error()
249
+ elif error_type == "timeout":
250
+ self.display_timeout_error()
251
+ else:
252
+ self.display_generic_error(display_msg)
253
+
254
+ def handle_streaming_error(self, error_data: dict):
255
+ """Handle errors from streaming API (type: "error" chunks)"""
256
+ error_msg = error_data.get("message", "Unknown error occurred")
257
+
258
+ # Parse the error
259
+ error_type, display_msg, upgrade_url = self.parse_error_message(error_msg)
260
+
261
+ # Display appropriate error
262
+ if error_type == "rate_limit":
263
+ match = re.search(r'(\d+)/(\d+) requests', error_msg)
264
+ if match:
265
+ self.display_rate_limit_error(match.group(1), match.group(2))
266
+ else:
267
+ self.display_rate_limit_error()
268
+ elif error_type == "server":
269
+ self.display_server_error()
270
+ elif error_type == "auth":
271
+ self.display_auth_error()
272
+ elif error_type == "connection":
273
+ self.display_connection_error()
274
+ elif error_type == "timeout":
275
+ self.display_timeout_error()
276
+ else:
277
+ self.display_generic_error(error_msg)
@@ -0,0 +1,156 @@
1
+ import functools
2
+ import sys
3
+ from typing import Callable
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ import httpx
7
+
8
+ console = Console()
9
+
10
+
11
+ class CodeMateError(Exception):
12
+ """Base exception for CodeMate CLI"""
13
+ pass
14
+
15
+
16
+ class APIError(CodeMateError):
17
+ """API-related errors"""
18
+ pass
19
+
20
+
21
+ class ConfigError(CodeMateError):
22
+ """Configuration-related errors"""
23
+ pass
24
+
25
+
26
+ class AuthenticationError(APIError):
27
+ """Authentication failed"""
28
+ pass
29
+
30
+
31
+ class NetworkError(APIError):
32
+ """Network connectivity issues"""
33
+ pass
34
+
35
+
36
+ def handle_errors(func: Callable) -> Callable:
37
+ """Decorator to handle errors gracefully in CLI commands"""
38
+
39
+ @functools.wraps(func)
40
+ def wrapper(*args, **kwargs):
41
+ try:
42
+ return func(*args, **kwargs)
43
+
44
+ except httpx.HTTPStatusError as e:
45
+ if e.response.status_code == 401:
46
+ console.print(Panel(
47
+ "[red]Authentication failed![/red]\n\n"
48
+ "Your API key may be invalid or expired.\n"
49
+ "Please set a valid API key:\n\n"
50
+ "[cyan]codemate config set-key YOUR_API_KEY[/cyan]",
51
+ title="[bold red]Authentication Error[/bold red]",
52
+ border_style="red"
53
+ ))
54
+ elif e.response.status_code == 429:
55
+ console.print(Panel(
56
+ "[red]Rate limit exceeded![/red]\n\n"
57
+ "You've made too many requests. Please wait a moment and try again.",
58
+ title="[bold red]Rate Limit[/bold red]",
59
+ border_style="red"
60
+ ))
61
+ elif e.response.status_code == 500:
62
+ console.print(Panel(
63
+ "[red]Server error![/red]\n\n"
64
+ "The API server encountered an error.\n"
65
+ "Please try again later or contact support.",
66
+ title="[bold red]Server Error[/bold red]",
67
+ border_style="red"
68
+ ))
69
+ else:
70
+ console.print(Panel(
71
+ f"[red]HTTP Error {e.response.status_code}[/red]\n\n"
72
+ f"{str(e)}",
73
+ title="[bold red]Request Failed[/bold red]",
74
+ border_style="red"
75
+ ))
76
+ sys.exit(1)
77
+
78
+ except httpx.ConnectError:
79
+ console.print(Panel(
80
+ "[red]Connection failed![/red]\n\n"
81
+ "Could not connect to the API server.\n"
82
+ "Please check:\n"
83
+ " • Is the server running?\n"
84
+ " • Is the endpoint correct?\n"
85
+ " • Do you have network connectivity?\n\n"
86
+ f"Current endpoint: [cyan]{kwargs.get('endpoint', 'http://localhost:45223')}[/cyan]\n\n"
87
+ "Set a different endpoint:\n"
88
+ "[cyan]codemate config set-endpoint http://your-server:port[/cyan]",
89
+ title="[bold red]Connection Error[/bold red]",
90
+ border_style="red"
91
+ ))
92
+ sys.exit(1)
93
+
94
+ except httpx.TimeoutException:
95
+ console.print(Panel(
96
+ "[red]Request timed out![/red]\n\n"
97
+ "The request took too long to complete.\n"
98
+ "This might be due to:\n"
99
+ " • Slow network connection\n"
100
+ " • Server overload\n"
101
+ " • Large request processing\n\n"
102
+ "Please try again.",
103
+ title="[bold red]Timeout Error[/bold red]",
104
+ border_style="red"
105
+ ))
106
+ sys.exit(1)
107
+
108
+ except ConfigError as e:
109
+ console.print(Panel(
110
+ f"[red]Configuration error![/red]\n\n{str(e)}",
111
+ title="[bold red]Config Error[/bold red]",
112
+ border_style="red"
113
+ ))
114
+ sys.exit(1)
115
+
116
+ except KeyboardInterrupt:
117
+ console.print("\n[yellow]Operation cancelled by user.[/yellow]")
118
+ sys.exit(0)
119
+
120
+ except Exception as e:
121
+ console.print(Panel(
122
+ f"[red]Unexpected error![/red]\n\n"
123
+ f"{type(e).__name__}: {str(e)}\n\n"
124
+ "If this persists, please report the issue.",
125
+ title="[bold red]Error[/bold red]",
126
+ border_style="red"
127
+ ))
128
+ if console.is_terminal:
129
+ import traceback
130
+ console.print("\n[dim]Full traceback:[/dim]")
131
+ traceback.print_exc()
132
+ sys.exit(1)
133
+
134
+ return wrapper
135
+
136
+
137
+ def format_error_message(error: Exception) -> str:
138
+ """Format an error message for display"""
139
+ error_type = type(error).__name__
140
+ return f"{error_type}: {str(error)}"
141
+
142
+
143
+ def is_network_error(error: Exception) -> bool:
144
+ """Check if an error is network-related"""
145
+ return isinstance(error, (
146
+ httpx.ConnectError,
147
+ httpx.TimeoutException,
148
+ httpx.NetworkError,
149
+ ))
150
+
151
+
152
+ def is_auth_error(error: Exception) -> bool:
153
+ """Check if an error is authentication-related"""
154
+ if isinstance(error, httpx.HTTPStatusError):
155
+ return error.response.status_code == 401
156
+ return isinstance(error, AuthenticationError)
@@ -0,0 +1,111 @@
1
+ """
2
+ Utility functions for parsing and handling knowledge base mentions in user input
3
+ """
4
+ import re
5
+ from typing import Dict, List, Optional, Tuple
6
+
7
+
8
+ def parse_kb_mentions(user_input: str, available_kbs: List[Dict]) -> Tuple[str, List[Dict], List[str]]:
9
+ """
10
+ Parse @kb_name mentions from user input and build context
11
+
12
+ Args:
13
+ user_input: The user's input message
14
+ available_kbs: List of available knowledge bases
15
+
16
+ Returns:
17
+ Tuple of (modified_message, context_list, invalid_kb_names)
18
+ - modified_message: Message with KB context XML tags inserted
19
+ - context_list: List of context objects for API
20
+ - invalid_kb_names: List of KB names that were mentioned but not found
21
+ """
22
+ # Find all @mentions in the input (supporting hyphens, dots, and word characters)
23
+ mentions = re.findall(r'@([\w.-]+)', user_input)
24
+
25
+ if not mentions:
26
+ return user_input, [], []
27
+
28
+ # Create a mapping of KB names to KB objects
29
+ kb_map = {kb['name']: kb for kb in available_kbs}
30
+
31
+ context_list = []
32
+ invalid_kbs = []
33
+ modified_message = user_input
34
+
35
+ # Process each mention
36
+ for kb_name in mentions:
37
+ if kb_name in kb_map:
38
+ kb = kb_map[kb_name]
39
+
40
+ # Build context object
41
+ context_obj = {
42
+ "type": kb.get("type", "codebase"),
43
+ "kbid": kb["id"],
44
+ "name": kb["name"],
45
+ "id": kb["id"]
46
+ }
47
+
48
+ # Only add if not already in context list
49
+ if not any(ctx["id"] == context_obj["id"] for ctx in context_list):
50
+ context_list.append(context_obj)
51
+
52
+ # Build XML context tag
53
+ context_xml = (
54
+ f"<cm:context>"
55
+ f"<cm:context:name>{kb['name']}</cm:context:name>"
56
+ f"<cm:context:type>{kb.get('type', 'codebase')}</cm:context:type>"
57
+ f"<cm:context:id>{kb['id']}</cm:context:id>"
58
+ f"<cm:context:content></cm:context:content>"
59
+ f"</cm:context>"
60
+ )
61
+
62
+ # Replace the @mention with context XML + @mention
63
+ # This ensures the mention appears once in the message
64
+ pattern = f"@{kb_name}"
65
+ if pattern in modified_message:
66
+ modified_message = modified_message.replace(
67
+ pattern,
68
+ f"{context_xml} ",
69
+ 1 # Replace only first occurrence
70
+ )
71
+ else:
72
+ invalid_kbs.append(kb_name)
73
+
74
+ # Remove any remaining @mentions that were invalid
75
+ for invalid_kb in invalid_kbs:
76
+ modified_message = modified_message.replace(f"@{invalid_kb}", f"[Invalid KB: @{invalid_kb}]")
77
+
78
+ return modified_message, context_list, invalid_kbs
79
+
80
+
81
+ def format_kb_context_message(
82
+ user_input: str,
83
+ available_kbs: List[Dict]
84
+ ) -> Tuple[Optional[str], Optional[List[Dict]], Optional[str]]:
85
+ """
86
+ Format user input with KB context for API
87
+
88
+ Args:
89
+ user_input: The user's input message
90
+ available_kbs: List of available knowledge bases
91
+
92
+ Returns:
93
+ Tuple of (message, context, error_message)
94
+ - If successful: (formatted_message, context_list, None)
95
+ - If error: (None, None, error_message)
96
+ """
97
+ modified_message, context_list, invalid_kbs = parse_kb_mentions(user_input, available_kbs)
98
+
99
+ # If there are invalid KB names, return error
100
+ if invalid_kbs:
101
+ error_msg = (
102
+ f"Invalid knowledge base name(s): {', '.join(['@' + kb for kb in invalid_kbs])}\n\n"
103
+ f"Use /listkb to see available knowledge bases."
104
+ )
105
+ return None, None, error_msg
106
+
107
+ # If no context found, return original message with empty context
108
+ if not context_list:
109
+ return user_input, None, None
110
+
111
+ return modified_message, context_list, None