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/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()