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.
- codemate/__init__.py +17 -0
- codemate/__main__.py +10 -0
- codemate/cli.py +815 -0
- codemate/client.py +311 -0
- codemate/commands/__init__.py +6 -0
- codemate/commands/chat.py +0 -0
- codemate/commands/config.py +103 -0
- codemate/commands/help.py +298 -0
- codemate/commands/kb_commands.py +749 -0
- codemate/config.py +233 -0
- codemate/ui/__init__.py +10 -0
- codemate/ui/markdown.py +212 -0
- codemate/ui/renderer.py +159 -0
- codemate/ui/streaming.py +436 -0
- codemate/utils/__init__.py +21 -0
- codemate/utils/auth.py +164 -0
- codemate/utils/error_handler.py +277 -0
- codemate/utils/errors.py +156 -0
- codemate/utils/kb_parser.py +111 -0
- codemate_cli-1.0.0.dist-info/METADATA +452 -0
- codemate_cli-1.0.0.dist-info/RECORD +25 -0
- codemate_cli-1.0.0.dist-info/WHEEL +5 -0
- codemate_cli-1.0.0.dist-info/entry_points.txt +3 -0
- codemate_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- codemate_cli-1.0.0.dist-info/top_level.txt +1 -0
codemate/cli.py
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import platform
|
|
7
|
+
import httpx
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich import box
|
|
15
|
+
from typing import Optional
|
|
16
|
+
import uuid
|
|
17
|
+
from prompt_toolkit import PromptSession
|
|
18
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
19
|
+
|
|
20
|
+
from codemate.config import Config
|
|
21
|
+
from codemate.client import SyncCodeMateClient
|
|
22
|
+
from codemate.ui.streaming import StreamingHandler
|
|
23
|
+
from codemate.utils.errors import handle_errors
|
|
24
|
+
from codemate.utils.kb_parser import format_kb_context_message
|
|
25
|
+
from codemate.utils.error_handler import ErrorHandler
|
|
26
|
+
from codemate.commands.kb_commands import KBCommands
|
|
27
|
+
from prompt_toolkit import PromptSession
|
|
28
|
+
from prompt_toolkit.formatted_text import HTML
|
|
29
|
+
from prompt_toolkit.styles import Style
|
|
30
|
+
|
|
31
|
+
# Define custom style for the input
|
|
32
|
+
custom_style = Style.from_dict({
|
|
33
|
+
'prompt': '#48AEF3 bold',
|
|
34
|
+
'input': '#FFFFFF',
|
|
35
|
+
})
|
|
36
|
+
console = Console()
|
|
37
|
+
config = Config()
|
|
38
|
+
|
|
39
|
+
LOGIN_URL = "https://identity.codemate.ai/?app=https://cli.codemate.build"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def show_welcome_banner():
|
|
43
|
+
"""Display elegant welcome banner when entering CLI"""
|
|
44
|
+
banner = r"""
|
|
45
|
+
██████╗ ██████╗ ██████╗ ███████╗███╗ ███╗ █████╗ ████████╗ ███████╗
|
|
46
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝████╗ ████║██╔══██╗╚══██╔══╝ ██╔════╝
|
|
47
|
+
██║ ██║ ██║██║ ██║█████╗ ██╔████╔██║███████║ ██║ █████╗
|
|
48
|
+
██║ ██║ ██║██║ ██║██╔══╝ ██║╚██╔╝██║██╔══██║ ██║ ██╔══╝
|
|
49
|
+
╚██████╗╚██████╔╝██████╔╝███████╗██║ ╚═╝ ██║██║ ██║ ██║ ███████╗
|
|
50
|
+
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
|
51
|
+
|
|
52
|
+
Your AI-Powered Coding Assistant
|
|
53
|
+
Version 1.0.0
|
|
54
|
+
"""
|
|
55
|
+
console.print(banner, style="bold cyan")
|
|
56
|
+
console.print()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def check_version_file() -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if version.txt exists at ~/.codemate/meta/subsystem_version.txt
|
|
62
|
+
Returns True if exists, False otherwise
|
|
63
|
+
"""
|
|
64
|
+
version_path = Path.home() / ".codemate" / "meta" / "subsystem_version.txt"
|
|
65
|
+
|
|
66
|
+
if not version_path.exists():
|
|
67
|
+
console.print()
|
|
68
|
+
console.print(Panel(
|
|
69
|
+
"[red]⚠️ Setup Not Installed Correctly[/red]\n\n"
|
|
70
|
+
# f"Required file not found:\n[cyan]{version_path}[/cyan]\n\n"
|
|
71
|
+
"Please run the CodeMate setup process first.",
|
|
72
|
+
title="[bold red]Installation Error[/bold red]",
|
|
73
|
+
border_style="red",
|
|
74
|
+
padding=(1, 2)
|
|
75
|
+
))
|
|
76
|
+
console.print()
|
|
77
|
+
|
|
78
|
+
# Log error
|
|
79
|
+
# import logging
|
|
80
|
+
# logging.error(f"Version file not found at {version_path}")
|
|
81
|
+
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_port_accessible(port: int, host: str = "127.0.0.1") -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Check if a port is accessible
|
|
90
|
+
Returns True if accessible, False otherwise
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
94
|
+
sock.settimeout(2)
|
|
95
|
+
result = sock.connect_ex((host, port))
|
|
96
|
+
sock.close()
|
|
97
|
+
return result == 0
|
|
98
|
+
except Exception as e:
|
|
99
|
+
console.print(f"[dim]Error checking port {port}: {e}[/dim]")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def start_initiate_process() -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Start the initiate.py process to set up the required services
|
|
106
|
+
Returns True if started successfully, False otherwise
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
codemate_dir = Path.home() / ".codemate"
|
|
110
|
+
bin_dir = codemate_dir / "bin"
|
|
111
|
+
initiate_path = bin_dir / "initiate.py"
|
|
112
|
+
|
|
113
|
+
if platform.system() == "Windows":
|
|
114
|
+
python_path = codemate_dir / "bin" / "environment" / "python.exe"
|
|
115
|
+
else:
|
|
116
|
+
python_path = codemate_dir / "bin" / "environment" / "bin" / "python"
|
|
117
|
+
|
|
118
|
+
if not python_path.exists():
|
|
119
|
+
console.print(f"[red]❌ Python not found at: {python_path}[/red]")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
if not initiate_path.exists():
|
|
123
|
+
console.print(f"[red]❌ initiate.py not found at: {initiate_path}[/red]")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
console.print("[yellow]Starting CodeMate services...[/yellow]")
|
|
127
|
+
|
|
128
|
+
# Proper flags to prevent any window from appearing on Windows
|
|
129
|
+
if platform.system() == "Windows":
|
|
130
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
131
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
132
|
+
startupinfo.wShowWindow = subprocess.SW_HIDE
|
|
133
|
+
creation_flags = subprocess.CREATE_NO_WINDOW
|
|
134
|
+
else:
|
|
135
|
+
startupinfo = None
|
|
136
|
+
creation_flags = 0
|
|
137
|
+
|
|
138
|
+
# Start initiate.py as a child process
|
|
139
|
+
process = subprocess.Popen(
|
|
140
|
+
[str(python_path), str(initiate_path)],
|
|
141
|
+
cwd=str(bin_dir),
|
|
142
|
+
text=True,
|
|
143
|
+
bufsize=1,
|
|
144
|
+
creationflags=creation_flags,
|
|
145
|
+
startupinfo=startupinfo
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if process.poll() is None:
|
|
149
|
+
console.print("[green]✓ CodeMate services started successfully[/green]")
|
|
150
|
+
|
|
151
|
+
# Wait a few seconds for services to initialize
|
|
152
|
+
import time
|
|
153
|
+
console.print("[yellow]Waiting for services to initialize...[/yellow]")
|
|
154
|
+
time.sleep(30)
|
|
155
|
+
|
|
156
|
+
return True
|
|
157
|
+
else:
|
|
158
|
+
console.print(f"[red]❌ Failed to start services (exit code: {process.returncode})[/red]")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
console.print(f"[red]❌ Failed to start initiate.py: {e}[/red]")
|
|
163
|
+
# import logging
|
|
164
|
+
# logging.error(f"Failed to start initiate.py: {e}")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def verify_setup_and_services() -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Verify setup installation and service accessibility
|
|
171
|
+
Returns True if all checks pass, False otherwise
|
|
172
|
+
"""
|
|
173
|
+
# Step 1: Check version file
|
|
174
|
+
console.print("[cyan]Checking installation...[/cyan]")
|
|
175
|
+
if not check_version_file():
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
console.print("[green]✓ Installation verified[/green]")
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
# Step 2: Check ports
|
|
182
|
+
console.print("[cyan]Checking services...[/cyan]")
|
|
183
|
+
http_port_ok = check_port_accessible(45223)
|
|
184
|
+
socket_port_ok = check_port_accessible(45224)
|
|
185
|
+
|
|
186
|
+
if http_port_ok and socket_port_ok:
|
|
187
|
+
console.print("[green]✓ Server is accessible[/green]")
|
|
188
|
+
# console.print("[green]✓ Socket port 45224: accessible[/green]")
|
|
189
|
+
console.print()
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
# If ports are not accessible, show status and attempt to start services
|
|
193
|
+
if not http_port_ok and not socket_port_ok:
|
|
194
|
+
console.print("[yellow]⚠ Server not accessible[/yellow]")
|
|
195
|
+
# if not socket_port_ok:
|
|
196
|
+
# console.print("[yellow]⚠ Socket port 45224: not accessible[/yellow]")
|
|
197
|
+
|
|
198
|
+
console.print()
|
|
199
|
+
console.print(Panel(
|
|
200
|
+
"[yellow]⚠️ Required services are not running[/yellow]\n\n"
|
|
201
|
+
"Attempting to start CodeMate services...",
|
|
202
|
+
border_style="yellow",
|
|
203
|
+
padding=(1, 2)
|
|
204
|
+
))
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
# Attempt to start services
|
|
208
|
+
if not start_initiate_process():
|
|
209
|
+
console.print()
|
|
210
|
+
console.print(Panel(
|
|
211
|
+
"[red]❌ Failed to start CodeMate services[/red]\n\n"
|
|
212
|
+
"Please ensure:\n"
|
|
213
|
+
"1. CodeMate is properly installed\n"
|
|
214
|
+
"2. No other processes are using ports 45223 and 45224\n"
|
|
215
|
+
"3. You have proper permissions\n\n"
|
|
216
|
+
"Try running the setup process again.",
|
|
217
|
+
title="[bold red]Service Error[/bold red]",
|
|
218
|
+
border_style="red",
|
|
219
|
+
padding=(1, 2)
|
|
220
|
+
))
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
# Verify ports are now accessible
|
|
224
|
+
http_port_ok = check_port_accessible(45223)
|
|
225
|
+
socket_port_ok = check_port_accessible(45224)
|
|
226
|
+
|
|
227
|
+
if http_port_ok and socket_port_ok:
|
|
228
|
+
console.print("[green]✓ Services started successfully[/green]")
|
|
229
|
+
console.print()
|
|
230
|
+
return True
|
|
231
|
+
else:
|
|
232
|
+
console.print("[red]❌ Services started but ports are still not accessible[/red]")
|
|
233
|
+
console.print("[yellow]Please check your firewall settings or try restarting[/yellow]")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def send_session_to_backend(session_id: str):
|
|
238
|
+
"""
|
|
239
|
+
Send the session token to the local backend server on port 45223.
|
|
240
|
+
"""
|
|
241
|
+
if not session_id:
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# We use a short timeout as this is a local call and we don't want to block the CLI
|
|
246
|
+
with httpx.Client(timeout=5.0) as client:
|
|
247
|
+
response = client.post(
|
|
248
|
+
"http://localhost:45223/set_session",
|
|
249
|
+
json={"session_id": session_id}
|
|
250
|
+
)
|
|
251
|
+
if response.status_code == 200:
|
|
252
|
+
# console.print("[dim]✓ Session synced with backend[/dim]")
|
|
253
|
+
pass
|
|
254
|
+
else:
|
|
255
|
+
console.print(f"[dim]Warning: Backend session sync failed (Status: {response.status_code})[/dim]")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
# Silently fail if backend is not running or other connection issues
|
|
258
|
+
# to avoid interrupting the user flow
|
|
259
|
+
# console.print(f"[dim]Note: Could not sync session with backend: {e}[/dim]")
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def show_login_required():
|
|
264
|
+
"""Display login required message and get token from user"""
|
|
265
|
+
login_panel = Panel(
|
|
266
|
+
f"[yellow]🔐 Login Required[/yellow]\n\n"
|
|
267
|
+
f"Please visit this URL to login:\n\n"
|
|
268
|
+
f"[cyan]{LOGIN_URL}[/cyan]",
|
|
269
|
+
title="[bold yellow]Authentication[/bold yellow]",
|
|
270
|
+
border_style="yellow",
|
|
271
|
+
padding=(1, 2)
|
|
272
|
+
)
|
|
273
|
+
console.print(login_panel)
|
|
274
|
+
console.print()
|
|
275
|
+
|
|
276
|
+
# Get token from user
|
|
277
|
+
token = console.input("[cyan]Enter your token:[/cyan] ").strip()
|
|
278
|
+
|
|
279
|
+
if token:
|
|
280
|
+
config.save_session(token)
|
|
281
|
+
send_session_to_backend(token)
|
|
282
|
+
console.print()
|
|
283
|
+
console.print(Panel(
|
|
284
|
+
"[green]✓[/green] Token saved successfully!\n\n"
|
|
285
|
+
"You can now use CodeMate CLI. Run codemate-cli",
|
|
286
|
+
border_style="green",
|
|
287
|
+
title="[bold green]Success[/bold green]",
|
|
288
|
+
padding=(1, 2)
|
|
289
|
+
))
|
|
290
|
+
return True
|
|
291
|
+
else:
|
|
292
|
+
console.print("[red]No token provided. Exiting...[/red]")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_user_input_simple_border(console: Console, session: PromptSession) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Simple box with all 4 borders - Multi-line display without truncation
|
|
299
|
+
Long inputs wrap to next lines, all contained within the box
|
|
300
|
+
"""
|
|
301
|
+
width = console.width - 4
|
|
302
|
+
|
|
303
|
+
# Top border
|
|
304
|
+
console.print()
|
|
305
|
+
console.print(f"[#48AEF3]╭{'─' * width}╮[/#48AEF3]")
|
|
306
|
+
|
|
307
|
+
# Input line with left border and prompt
|
|
308
|
+
user_input = session.prompt(
|
|
309
|
+
[
|
|
310
|
+
('#48AEF3', '│ '),
|
|
311
|
+
('#48AEF3 bold', 'You ❯ '),
|
|
312
|
+
],
|
|
313
|
+
style=custom_style,
|
|
314
|
+
multiline=False,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Calculate how many lines the input took
|
|
318
|
+
prompt_length = len('│ You ❯ ')
|
|
319
|
+
total_length = prompt_length + len(user_input)
|
|
320
|
+
lines_used = (total_length // console.width) + 1
|
|
321
|
+
|
|
322
|
+
# Move cursor up to the first input line
|
|
323
|
+
import sys
|
|
324
|
+
for _ in range(lines_used):
|
|
325
|
+
sys.stdout.write('\033[F')
|
|
326
|
+
|
|
327
|
+
sys.stdout.write('\r')
|
|
328
|
+
sys.stdout.flush()
|
|
329
|
+
|
|
330
|
+
# Now redraw with proper box formatting
|
|
331
|
+
# Split the input into chunks that fit in the box
|
|
332
|
+
max_chars_per_line = width - 4 # Account for borders and padding
|
|
333
|
+
|
|
334
|
+
# First line has "You ❯ " so less space
|
|
335
|
+
first_line_max = max_chars_per_line - len("You ❯ ")
|
|
336
|
+
|
|
337
|
+
if len(user_input) <= first_line_max:
|
|
338
|
+
# Single line - simple case
|
|
339
|
+
remaining_padding = width - len(f" You ❯ {user_input} ") - 2
|
|
340
|
+
console.print(f"[#48AEF3]│[/#48AEF3] [bold #48AEF3]You ❯[/bold #48AEF3] {user_input}{' ' * max(0, remaining_padding)} [#48AEF3]│[/#48AEF3]")
|
|
341
|
+
else:
|
|
342
|
+
# Multi-line - split the input
|
|
343
|
+
# First line
|
|
344
|
+
first_part = user_input[:first_line_max]
|
|
345
|
+
remaining_text = user_input[first_line_max:]
|
|
346
|
+
|
|
347
|
+
padding = width - len(f" You ❯ {first_part} ") - 2
|
|
348
|
+
console.print(f"[#48AEF3]│[/#48AEF3] [bold #48AEF3]You ❯[/bold #48AEF3] {first_part}{' ' * max(0, padding)} [#48AEF3]│[/#48AEF3]")
|
|
349
|
+
|
|
350
|
+
# Subsequent lines (without "You ❯")
|
|
351
|
+
while remaining_text:
|
|
352
|
+
line_chunk = remaining_text[:max_chars_per_line]
|
|
353
|
+
remaining_text = remaining_text[max_chars_per_line:]
|
|
354
|
+
|
|
355
|
+
padding = width - len(f" {line_chunk} ") - 2
|
|
356
|
+
console.print(f"[#48AEF3]│[/#48AEF3] {line_chunk}{' ' * max(0, padding)} [#48AEF3]│[/#48AEF3]")
|
|
357
|
+
|
|
358
|
+
# Bottom border
|
|
359
|
+
console.print(f"[#48AEF3]╰{'─' * width}╯[/#48AEF3]")
|
|
360
|
+
console.print()
|
|
361
|
+
|
|
362
|
+
return user_input
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def check_authentication() -> bool:
|
|
366
|
+
"""Check if user is authenticated, prompt login if not"""
|
|
367
|
+
if not config.is_logged_in():
|
|
368
|
+
show_login_required()
|
|
369
|
+
return False
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def estimate_tokens(text: str) -> int:
|
|
374
|
+
"""Estimate token count as len(text) // 4"""
|
|
375
|
+
return len(text) // 4
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def show_startup_info():
|
|
379
|
+
"""Display startup information and available commands"""
|
|
380
|
+
# Create info panel
|
|
381
|
+
info_text = Text()
|
|
382
|
+
info_text.append("🚀 ", style="bold yellow")
|
|
383
|
+
info_text.append("Welcome to CodeMate CLI Interactive Mode\n\n", style="bold cyan")
|
|
384
|
+
info_text.append("You're now in the CodeMate environment. ", style="white")
|
|
385
|
+
info_text.append("Ask me anything!\n", style="white")
|
|
386
|
+
|
|
387
|
+
console.print(Panel(
|
|
388
|
+
info_text,
|
|
389
|
+
border_style="cyan",
|
|
390
|
+
box=box.DOUBLE,
|
|
391
|
+
padding=(1, 2)
|
|
392
|
+
))
|
|
393
|
+
console.print()
|
|
394
|
+
|
|
395
|
+
# Show quick commands
|
|
396
|
+
commands_table = Table(
|
|
397
|
+
show_header=True,
|
|
398
|
+
header_style="bold magenta",
|
|
399
|
+
border_style="blue",
|
|
400
|
+
box=box.ROUNDED,
|
|
401
|
+
title="🎯 Quick Commands",
|
|
402
|
+
title_style="bold yellow"
|
|
403
|
+
)
|
|
404
|
+
commands_table.add_column("Command", style="cyan", width=15)
|
|
405
|
+
commands_table.add_column("Description", style="white", width=45)
|
|
406
|
+
|
|
407
|
+
commands_table.add_row("/help", "Show available commands")
|
|
408
|
+
commands_table.add_row("/clear", "Clear conversation history")
|
|
409
|
+
commands_table.add_row("/listkb", "List available knowledge bases")
|
|
410
|
+
commands_table.add_row("/createkb", "Create a new knowledge base")
|
|
411
|
+
commands_table.add_row("/deletekb", "Delete a knowledge base")
|
|
412
|
+
commands_table.add_row("/logout", "Logout from CodeMate")
|
|
413
|
+
commands_table.add_row("exit", "Exit CodeMate CLI")
|
|
414
|
+
|
|
415
|
+
console.print(commands_table)
|
|
416
|
+
console.print()
|
|
417
|
+
|
|
418
|
+
# Show tips
|
|
419
|
+
tips = Panel(
|
|
420
|
+
"[yellow]💡 Tip:[/yellow] Use @kb_name to reference a knowledge base in your question.\n"
|
|
421
|
+
"[dim]Example: \"Explain the structure of @uicli\"[/dim]\n\n"
|
|
422
|
+
"[yellow]💬 Tip:[/yellow] Just type your question and press Enter for streaming responses!",
|
|
423
|
+
border_style="yellow",
|
|
424
|
+
box=box.ROUNDED,
|
|
425
|
+
padding=(1, 2)
|
|
426
|
+
)
|
|
427
|
+
console.print(tips)
|
|
428
|
+
console.print()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@click.command()
|
|
432
|
+
@click.option('--model', '-m', default='chat_c0_cli', help='AI model to use')
|
|
433
|
+
@click.option('--no-banner', is_flag=True, help='Skip welcome banner')
|
|
434
|
+
@handle_errors
|
|
435
|
+
def main(model: str, no_banner: bool):
|
|
436
|
+
"""
|
|
437
|
+
🚀 CodeMate CLI - Interactive AI Assistant
|
|
438
|
+
|
|
439
|
+
Enter the CodeMate environment and chat with AI.
|
|
440
|
+
Type 'exit' to leave.
|
|
441
|
+
"""
|
|
442
|
+
# Show welcome banner first
|
|
443
|
+
if not no_banner:
|
|
444
|
+
console.clear()
|
|
445
|
+
show_welcome_banner()
|
|
446
|
+
|
|
447
|
+
# Step 1 & 2: Verify setup and services BEFORE authentication
|
|
448
|
+
if not verify_setup_and_services():
|
|
449
|
+
# console.print("[red]Exiting due to setup/service errors...[/red]")
|
|
450
|
+
sys.exit(1)
|
|
451
|
+
|
|
452
|
+
# Step 3: Check authentication
|
|
453
|
+
if not check_authentication():
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Sync session with backend on startup if already logged in
|
|
457
|
+
session_id = config.get_session()
|
|
458
|
+
if session_id:
|
|
459
|
+
send_session_to_backend(session_id)
|
|
460
|
+
|
|
461
|
+
# Show startup info after all checks pass
|
|
462
|
+
if not no_banner:
|
|
463
|
+
show_startup_info()
|
|
464
|
+
|
|
465
|
+
# Initialize components
|
|
466
|
+
client = SyncCodeMateClient(config)
|
|
467
|
+
handler = StreamingHandler(console)
|
|
468
|
+
kb_commands = KBCommands(config, client)
|
|
469
|
+
session = PromptSession(history=InMemoryHistory())
|
|
470
|
+
conversation_history = []
|
|
471
|
+
current_model = model
|
|
472
|
+
|
|
473
|
+
# Load knowledge bases once at startup
|
|
474
|
+
try:
|
|
475
|
+
available_kbs = client.list_kbs()
|
|
476
|
+
kb_count = len(available_kbs) if available_kbs else 0
|
|
477
|
+
except Exception as e:
|
|
478
|
+
console.print(f"[yellow]⚠️ Could not load knowledge bases: {e}[/yellow]")
|
|
479
|
+
available_kbs = []
|
|
480
|
+
kb_count = 0
|
|
481
|
+
|
|
482
|
+
# Show ready status
|
|
483
|
+
console.print(Panel(
|
|
484
|
+
f"[green]✓[/green] Logged in\n"
|
|
485
|
+
f"[green]✓[/green] Connected to SERVER\n"
|
|
486
|
+
f"[green]✓[/green] Model: [cyan]auto[/cyan]\n"
|
|
487
|
+
f"[green]✓[/green] Knowledge Bases: [cyan]{kb_count} loaded[/cyan]\n"
|
|
488
|
+
f"[green]✓[/green] Ready to assist!",
|
|
489
|
+
border_style="green",
|
|
490
|
+
box=box.ROUNDED,
|
|
491
|
+
padding=(0, 2)
|
|
492
|
+
))
|
|
493
|
+
console.print()
|
|
494
|
+
console.print("[dim]═" * console.width + "[/dim]")
|
|
495
|
+
console.print()
|
|
496
|
+
|
|
497
|
+
error_handler = ErrorHandler(console)
|
|
498
|
+
|
|
499
|
+
# Main interactive loop
|
|
500
|
+
while True:
|
|
501
|
+
try:
|
|
502
|
+
# Check authentication before each message
|
|
503
|
+
if not config.is_logged_in():
|
|
504
|
+
console.print()
|
|
505
|
+
console.print(Panel(
|
|
506
|
+
"[red]⚠️ Session expired or logged out![/red]\n\n"
|
|
507
|
+
"Please login again to continue.",
|
|
508
|
+
border_style="red",
|
|
509
|
+
title="[bold red]Authentication Required[/bold red]"
|
|
510
|
+
))
|
|
511
|
+
show_login_required()
|
|
512
|
+
break
|
|
513
|
+
|
|
514
|
+
# Get user input with custom prompt
|
|
515
|
+
user_input = get_user_input_simple_border(console, session)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# Skip empty input
|
|
519
|
+
if not user_input.strip():
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
# Check for exit command
|
|
523
|
+
if user_input.lower().strip() in ['exit', 'quit', 'bye']:
|
|
524
|
+
show_goodbye()
|
|
525
|
+
break
|
|
526
|
+
|
|
527
|
+
# Handle special commands
|
|
528
|
+
if user_input.startswith('/'):
|
|
529
|
+
should_continue = asyncio.run(handle_special_command_async(
|
|
530
|
+
user_input.lower().strip(),
|
|
531
|
+
conversation_history,
|
|
532
|
+
current_model,
|
|
533
|
+
client,
|
|
534
|
+
available_kbs,
|
|
535
|
+
kb_commands
|
|
536
|
+
))
|
|
537
|
+
if not should_continue:
|
|
538
|
+
break
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
# Parse KB mentions and format message
|
|
542
|
+
formatted_message, context, error_msg = format_kb_context_message(
|
|
543
|
+
user_input,
|
|
544
|
+
available_kbs
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# If there's an error (invalid KB name), show it and continue
|
|
548
|
+
if error_msg:
|
|
549
|
+
console.print()
|
|
550
|
+
console.print(Panel(
|
|
551
|
+
f"[red]❌ {error_msg}[/red]",
|
|
552
|
+
border_style="red",
|
|
553
|
+
title="[bold red]Invalid Knowledge Base[/bold red]",
|
|
554
|
+
padding=(1, 2)
|
|
555
|
+
))
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
# Add separator for clarity
|
|
559
|
+
console.print()
|
|
560
|
+
console.print("[dim]─" * console.width + "[/dim]")
|
|
561
|
+
console.print()
|
|
562
|
+
|
|
563
|
+
# Show attached KB context if any
|
|
564
|
+
if context:
|
|
565
|
+
kb_names = [ctx['name'] for ctx in context]
|
|
566
|
+
console.print(Panel(
|
|
567
|
+
f"[cyan]📚 Attached Knowledge Base(s):[/cyan] {', '.join(['@' + name for name in kb_names])}",
|
|
568
|
+
border_style="cyan",
|
|
569
|
+
padding=(0, 1)
|
|
570
|
+
))
|
|
571
|
+
console.print()
|
|
572
|
+
|
|
573
|
+
# Build user message for history
|
|
574
|
+
user_message = {
|
|
575
|
+
"role": "user",
|
|
576
|
+
"content": formatted_message,
|
|
577
|
+
"context": context if context else [],
|
|
578
|
+
"web_search": False
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# Add to conversation history
|
|
582
|
+
conversation_history.append(user_message)
|
|
583
|
+
|
|
584
|
+
# Calculate total tokens and warn if high
|
|
585
|
+
total_tokens = sum(estimate_tokens(msg.get("content", "")) for msg in conversation_history)
|
|
586
|
+
if total_tokens > 80000:
|
|
587
|
+
console.print(Panel(
|
|
588
|
+
"[yellow]⚠️ High token usage detected![/yellow]\n\n"
|
|
589
|
+
"Consider using /clear to clear conversation history.",
|
|
590
|
+
border_style="yellow",
|
|
591
|
+
padding=(1, 2)
|
|
592
|
+
))
|
|
593
|
+
|
|
594
|
+
# Generate unique conversation_id for this request
|
|
595
|
+
conversation_id = str(uuid.uuid4())
|
|
596
|
+
|
|
597
|
+
# Show AI thinking indicator
|
|
598
|
+
console.print("[AI] ❯ ", style="bold green", end="")
|
|
599
|
+
|
|
600
|
+
# Get streaming response with context
|
|
601
|
+
try:
|
|
602
|
+
response_text, assistant_entry, tool_results = handler.handle_streaming_response(
|
|
603
|
+
client.chat_stream(
|
|
604
|
+
formatted_message,
|
|
605
|
+
model=current_model,
|
|
606
|
+
conversation_history=conversation_history[:-1],
|
|
607
|
+
conversation_id=conversation_id,
|
|
608
|
+
context=context
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Add assistant response to history
|
|
613
|
+
if assistant_entry:
|
|
614
|
+
conversation_history.append(assistant_entry)
|
|
615
|
+
|
|
616
|
+
# Add tool results to history if any
|
|
617
|
+
if tool_results:
|
|
618
|
+
conversation_history.extend(tool_results)
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
error_handler.handle_error(e)
|
|
622
|
+
# Remove failed user message from history
|
|
623
|
+
conversation_history.pop()
|
|
624
|
+
|
|
625
|
+
console.print()
|
|
626
|
+
console.print("[dim]─" * console.width + "[/dim]")
|
|
627
|
+
|
|
628
|
+
except KeyboardInterrupt:
|
|
629
|
+
console.print("\n")
|
|
630
|
+
confirm = console.input("[yellow]Do you want to exit? (y/n):[/yellow] ")
|
|
631
|
+
if confirm.lower() in ['y', 'yes']:
|
|
632
|
+
show_goodbye()
|
|
633
|
+
break
|
|
634
|
+
continue
|
|
635
|
+
|
|
636
|
+
except EOFError:
|
|
637
|
+
show_goodbye()
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
async def handle_special_command_async(
|
|
642
|
+
command: str,
|
|
643
|
+
conversation_history: list,
|
|
644
|
+
current_model: str,
|
|
645
|
+
client,
|
|
646
|
+
available_kbs: list,
|
|
647
|
+
kb_commands
|
|
648
|
+
) -> bool:
|
|
649
|
+
"""
|
|
650
|
+
Handle special commands starting with / (ASYNC version)
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
True to continue, False to exit
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
if command == '/help':
|
|
657
|
+
show_help_menu()
|
|
658
|
+
|
|
659
|
+
elif command == '/clear':
|
|
660
|
+
conversation_history.clear()
|
|
661
|
+
console.print(Panel(
|
|
662
|
+
"[green]✓[/green] Conversation history cleared!",
|
|
663
|
+
border_style="green"
|
|
664
|
+
))
|
|
665
|
+
|
|
666
|
+
elif command == '/logout':
|
|
667
|
+
confirm = console.input("[yellow]Are you sure you want to logout? (y/n):[/yellow] ")
|
|
668
|
+
if confirm.lower() in ['y', 'yes']:
|
|
669
|
+
config.clear_session()
|
|
670
|
+
console.print(Panel(
|
|
671
|
+
"[green]✓[/green] Logged out successfully!\n\n"
|
|
672
|
+
"Run [cyan]codemate-cli[/cyan] again to login.",
|
|
673
|
+
border_style="green",
|
|
674
|
+
title="[bold green]Logged Out[/bold green]"
|
|
675
|
+
))
|
|
676
|
+
return False
|
|
677
|
+
|
|
678
|
+
elif command == '/createkb':
|
|
679
|
+
try:
|
|
680
|
+
await kb_commands.create_kb()
|
|
681
|
+
# Refresh KB list after creation
|
|
682
|
+
kbs = client.list_kbs()
|
|
683
|
+
available_kbs.clear()
|
|
684
|
+
available_kbs.extend(kbs)
|
|
685
|
+
except Exception as e:
|
|
686
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
687
|
+
|
|
688
|
+
elif command == '/deletekb':
|
|
689
|
+
try:
|
|
690
|
+
await kb_commands.delete_kb()
|
|
691
|
+
# Refresh KB list after deletion
|
|
692
|
+
kbs = client.list_kbs()
|
|
693
|
+
available_kbs.clear()
|
|
694
|
+
available_kbs.extend(kbs)
|
|
695
|
+
except Exception as e:
|
|
696
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
697
|
+
|
|
698
|
+
elif command == '/listkb':
|
|
699
|
+
try:
|
|
700
|
+
# Refresh KB list
|
|
701
|
+
kbs = client.list_kbs()
|
|
702
|
+
available_kbs.clear()
|
|
703
|
+
available_kbs.extend(kbs)
|
|
704
|
+
|
|
705
|
+
if not kbs:
|
|
706
|
+
console.print("[yellow]No knowledge bases found.[/yellow]")
|
|
707
|
+
else:
|
|
708
|
+
table = Table(
|
|
709
|
+
title="📚 Available Knowledge Bases",
|
|
710
|
+
show_header=True,
|
|
711
|
+
header_style="bold magenta",
|
|
712
|
+
border_style="blue",
|
|
713
|
+
box=box.ROUNDED
|
|
714
|
+
)
|
|
715
|
+
table.add_column("Name", style="cyan", width=20)
|
|
716
|
+
table.add_column("Type", style="yellow", width=12)
|
|
717
|
+
table.add_column("Status", style="green", width=10)
|
|
718
|
+
table.add_column("Description", style="white", width=40)
|
|
719
|
+
|
|
720
|
+
for kb in kbs:
|
|
721
|
+
kb_name = f"@{kb.get('name', '')}"
|
|
722
|
+
kb_type = kb.get('type', 'unknown')
|
|
723
|
+
kb_status = kb.get('status', 'unknown')
|
|
724
|
+
kb_desc = kb.get('description', '') or '(no description)'
|
|
725
|
+
|
|
726
|
+
# Status indicator
|
|
727
|
+
status_color = "green" if kb_status == "ready" else "yellow"
|
|
728
|
+
status_display = f"[{status_color}]●[/{status_color}] {kb_status}"
|
|
729
|
+
|
|
730
|
+
table.add_row(kb_name, kb_type, status_display, kb_desc)
|
|
731
|
+
|
|
732
|
+
console.print(table)
|
|
733
|
+
console.print()
|
|
734
|
+
console.print("[dim]💡 Tip: Use @kb_name in your messages to reference a knowledge base[/dim]")
|
|
735
|
+
console.print("[dim]Example: \"What files are in @uicli?\"[/dim]")
|
|
736
|
+
except Exception as e:
|
|
737
|
+
console.print(f"[red]❌ Error listing knowledge bases: {e}[/red]")
|
|
738
|
+
|
|
739
|
+
else:
|
|
740
|
+
console.print(f"[yellow]Unknown command: {command}[/yellow]")
|
|
741
|
+
console.print("[dim]Type /help to see available commands[/dim]")
|
|
742
|
+
|
|
743
|
+
return True
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def show_help_menu():
|
|
747
|
+
"""Display help menu"""
|
|
748
|
+
help_text = """
|
|
749
|
+
## Available Commands
|
|
750
|
+
|
|
751
|
+
### Chat Commands
|
|
752
|
+
- Just type your message and press Enter to chat with AI
|
|
753
|
+
- Use **@kb_name** to reference a knowledge base in your question
|
|
754
|
+
- Responses are streamed in real-time
|
|
755
|
+
|
|
756
|
+
### Special Commands
|
|
757
|
+
- `/help` - Show this help menu
|
|
758
|
+
- `/clear` - Clear conversation history
|
|
759
|
+
- `/listkb` - List available knowledge bases
|
|
760
|
+
- `/createkb` - Create a new knowledge base from a local directory
|
|
761
|
+
- `/deletekb` - Delete an existing knowledge base
|
|
762
|
+
- `/logout` - Logout from CodeMate
|
|
763
|
+
- `exit` or `quit` - Exit CodeMate CLI
|
|
764
|
+
|
|
765
|
+
### Knowledge Base Usage
|
|
766
|
+
Reference knowledge bases by typing **@** followed by the KB name:
|
|
767
|
+
- `@uicli` - Reference the uicli knowledge base
|
|
768
|
+
- You can reference multiple KBs: `Compare @kb1 and @kb2`
|
|
769
|
+
|
|
770
|
+
### Knowledge Base Management
|
|
771
|
+
- Use `/createkb` to index a local directory as a knowledge base
|
|
772
|
+
- Use `/deletekb` to remove a knowledge base
|
|
773
|
+
- Use `/listkb` to see all available knowledge bases
|
|
774
|
+
|
|
775
|
+
### Tips
|
|
776
|
+
- Press Ctrl+C to interrupt and get exit prompt
|
|
777
|
+
- Type naturally - no special formatting needed
|
|
778
|
+
- Use /createkb to index your codebase for better AI assistance
|
|
779
|
+
"""
|
|
780
|
+
console.print(Panel(
|
|
781
|
+
Markdown(help_text),
|
|
782
|
+
title="[bold cyan]Help Menu[/bold cyan]",
|
|
783
|
+
border_style="cyan",
|
|
784
|
+
padding=(1, 2)
|
|
785
|
+
))
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def show_goodbye():
|
|
789
|
+
"""Display goodbye message"""
|
|
790
|
+
console.print()
|
|
791
|
+
console.print(Panel(
|
|
792
|
+
Text.from_markup(
|
|
793
|
+
"\n[cyan]Thank you for using CodeMate CLI![/cyan]\n\n"
|
|
794
|
+
"👋 Goodbye! Come back anytime.\n",
|
|
795
|
+
justify="center"
|
|
796
|
+
),
|
|
797
|
+
border_style="cyan",
|
|
798
|
+
box=box.DOUBLE,
|
|
799
|
+
padding=(1, 2)
|
|
800
|
+
))
|
|
801
|
+
console.print()
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
# Configuration command (separate from main interactive mode)
|
|
805
|
+
@click.group(invoke_without_command=True)
|
|
806
|
+
@click.pass_context
|
|
807
|
+
def cli_group(ctx):
|
|
808
|
+
"""CodeMate CLI"""
|
|
809
|
+
if ctx.invoked_subcommand is None:
|
|
810
|
+
# If no subcommand, launch interactive mode
|
|
811
|
+
main.callback(model='chat_c0_cli', no_banner=False)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
if __name__ == "__main__":
|
|
815
|
+
cli_group()
|