minion-code 0.1.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 (59) hide show
  1. examples/advance_tui.py +508 -0
  2. examples/agent_with_todos.py +165 -0
  3. examples/file_freshness_example.py +97 -0
  4. examples/file_watching_example.py +110 -0
  5. examples/interruptible_tui.py +5 -0
  6. examples/message_response_children_demo.py +226 -0
  7. examples/rich_example.py +4 -0
  8. examples/simple_file_watching.py +57 -0
  9. examples/simple_tui.py +267 -0
  10. examples/simple_usage.py +69 -0
  11. minion_code/__init__.py +16 -0
  12. minion_code/agents/__init__.py +11 -0
  13. minion_code/agents/code_agent.py +320 -0
  14. minion_code/cli.py +502 -0
  15. minion_code/commands/__init__.py +90 -0
  16. minion_code/commands/clear_command.py +70 -0
  17. minion_code/commands/help_command.py +90 -0
  18. minion_code/commands/history_command.py +104 -0
  19. minion_code/commands/quit_command.py +32 -0
  20. minion_code/commands/status_command.py +115 -0
  21. minion_code/commands/tools_command.py +86 -0
  22. minion_code/commands/version_command.py +104 -0
  23. minion_code/components/Message.py +304 -0
  24. minion_code/components/MessageResponse.py +188 -0
  25. minion_code/components/PromptInput.py +534 -0
  26. minion_code/components/__init__.py +29 -0
  27. minion_code/screens/REPL.py +925 -0
  28. minion_code/screens/__init__.py +4 -0
  29. minion_code/services/__init__.py +50 -0
  30. minion_code/services/event_system.py +108 -0
  31. minion_code/services/file_freshness_service.py +582 -0
  32. minion_code/tools/__init__.py +69 -0
  33. minion_code/tools/bash_tool.py +58 -0
  34. minion_code/tools/file_edit_tool.py +238 -0
  35. minion_code/tools/file_read_tool.py +73 -0
  36. minion_code/tools/file_write_tool.py +36 -0
  37. minion_code/tools/glob_tool.py +58 -0
  38. minion_code/tools/grep_tool.py +105 -0
  39. minion_code/tools/ls_tool.py +65 -0
  40. minion_code/tools/multi_edit_tool.py +271 -0
  41. minion_code/tools/python_interpreter_tool.py +105 -0
  42. minion_code/tools/todo_read_tool.py +100 -0
  43. minion_code/tools/todo_write_tool.py +234 -0
  44. minion_code/tools/user_input_tool.py +53 -0
  45. minion_code/types.py +88 -0
  46. minion_code/utils/__init__.py +44 -0
  47. minion_code/utils/mcp_loader.py +211 -0
  48. minion_code/utils/todo_file_utils.py +110 -0
  49. minion_code/utils/todo_storage.py +149 -0
  50. minion_code-0.1.0.dist-info/METADATA +350 -0
  51. minion_code-0.1.0.dist-info/RECORD +59 -0
  52. minion_code-0.1.0.dist-info/WHEEL +5 -0
  53. minion_code-0.1.0.dist-info/entry_points.txt +4 -0
  54. minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
  55. minion_code-0.1.0.dist-info/top_level.txt +3 -0
  56. tests/__init__.py +1 -0
  57. tests/test_basic.py +20 -0
  58. tests/test_readonly_tools.py +102 -0
  59. tests/test_tools.py +83 -0
minion_code/cli.py ADDED
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ CLI interface for MinionCodeAgent using Typer
5
+
6
+ This CLI provides command-line arguments support including --dir and --verbose options.
7
+ """
8
+
9
+ import asyncio
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ import typer
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.text import Text
19
+ from rich.markdown import Markdown
20
+ from rich.table import Table
21
+ from rich.progress import Progress, SpinnerColumn, TextColumn
22
+ from rich.prompt import Prompt
23
+
24
+ # Add project root to path
25
+ sys.path.insert(0, str(Path(__file__).parent.parent))
26
+
27
+ from minion_code import MinionCodeAgent
28
+ from minion_code.commands import command_registry
29
+ from minion_code.utils.mcp_loader import MCPToolsLoader
30
+
31
+ app = typer.Typer(
32
+ name="minion-code",
33
+ help="🤖 MinionCodeAgent CLI - An AI-powered code assistant",
34
+ add_completion=False,
35
+ rich_markup_mode="rich"
36
+ )
37
+
38
+
39
+ class InterruptibleCLI:
40
+ """CLI with task interruption support using standard input."""
41
+
42
+ def __init__(self, verbose: bool = False, mcp_config: Optional[Path] = None):
43
+ self.agent = None
44
+ self.running = True
45
+ self.console = Console()
46
+ self.verbose = verbose
47
+ self.mcp_config = mcp_config
48
+ self.mcp_tools = []
49
+ self.mcp_loader = None
50
+ self.current_task = None
51
+ self.task_cancelled = False
52
+ self.interrupt_requested = False
53
+
54
+ async def setup(self):
55
+ """Setup the agent."""
56
+ with Progress(
57
+ SpinnerColumn(),
58
+ TextColumn("[progress.description]{task.description}"),
59
+ console=self.console,
60
+ ) as progress:
61
+ # Load MCP tools if config provided
62
+ mcp_task = None
63
+ if self.mcp_config:
64
+ mcp_task = progress.add_task("🔌 Loading MCP tools...", total=None)
65
+ try:
66
+ self.mcp_loader = MCPToolsLoader(self.mcp_config)
67
+ self.mcp_loader.load_config()
68
+ self.mcp_tools = await self.mcp_loader.load_all_tools()
69
+
70
+ if self.mcp_tools:
71
+ self.console.print(f"✅ Loaded {len(self.mcp_tools)} MCP tools")
72
+ else:
73
+ server_info = self.mcp_loader.get_server_info()
74
+ if server_info:
75
+ self.console.print(f"📋 Found {len(server_info)} MCP server(s) configured")
76
+ for name, info in server_info.items():
77
+ status = "disabled" if info['disabled'] else "enabled"
78
+ self.console.print(f" - {name}: {info['command']} ({status})")
79
+ else:
80
+ self.console.print("⚠️ No MCP servers found in config")
81
+
82
+ progress.update(mcp_task, completed=True)
83
+ except Exception as e:
84
+ self.console.print(f"❌ Failed to load MCP tools: {e}")
85
+ if self.verbose:
86
+ import traceback
87
+ self.console.print(f"[dim]{traceback.format_exc()}[/dim]")
88
+
89
+ agent_task = progress.add_task("🔧 Setting up MinionCodeAgent...", total=None)
90
+
91
+ self.agent = await MinionCodeAgent.create(
92
+ name="CLI Code Assistant",
93
+ llm="sonnet", # 使用更稳定的模型配置
94
+ additional_tools=self.mcp_tools if self.mcp_tools else None
95
+ )
96
+
97
+ progress.update(agent_task, completed=True)
98
+
99
+ # Show setup summary
100
+ total_tools = len(self.agent.tools)
101
+ mcp_count = len(self.mcp_tools)
102
+ builtin_count = total_tools - mcp_count
103
+
104
+ summary_text = f"✅ Agent ready with [bold green]{total_tools}[/bold green] tools!"
105
+ if mcp_count > 0:
106
+ summary_text += f"\n🔌 MCP tools: [bold cyan]{mcp_count}[/bold cyan]"
107
+ summary_text += f"\n🛠️ Built-in tools: [bold blue]{builtin_count}[/bold blue]"
108
+ summary_text += f"\n⚠️ [bold yellow]Press Ctrl+C during processing to interrupt tasks[/bold yellow]"
109
+
110
+ success_panel = Panel(
111
+ summary_text,
112
+ title="[bold green]Setup Complete[/bold green]",
113
+ border_style="green"
114
+ )
115
+ self.console.print(success_panel)
116
+
117
+ if self.verbose:
118
+ self.console.print(f"[dim]Working directory: {os.getcwd()}[/dim]")
119
+
120
+ def show_help(self):
121
+ """Show help information."""
122
+ help_table = Table(title="📚 MinionCode CLI Help", show_header=True, header_style="bold blue")
123
+ help_table.add_column("Command/Key", style="cyan", no_wrap=True)
124
+ help_table.add_column("Description", style="white")
125
+
126
+ help_table.add_row("help", "Show this help")
127
+ help_table.add_row("tools", "List available tools")
128
+ help_table.add_row("history", "Show conversation history")
129
+ help_table.add_row("clear", "Clear history")
130
+ help_table.add_row("quit", "Exit")
131
+ help_table.add_row("Ctrl+C", "Interrupt current task or exit")
132
+
133
+ self.console.print(help_table)
134
+ self.console.print("\n💡 [italic]Just type your message to chat with the AI agent![/italic]")
135
+ self.console.print("⚠️ [italic]During task processing, press Ctrl+C to interrupt the current task[/italic]")
136
+
137
+ async def process_input_with_interrupt(self, user_input: str):
138
+ """Process user input with interrupt support."""
139
+ self.task_cancelled = False
140
+ self.interrupt_requested = False
141
+
142
+ try:
143
+ # Create the actual processing task
144
+ async def processing_task():
145
+ response = await self.agent.run_async(user_input)
146
+ return response
147
+
148
+ # Start the task
149
+ self.current_task = asyncio.create_task(processing_task())
150
+
151
+ # Monitor for cancellation while task runs
152
+ while not self.current_task.done():
153
+ if self.interrupt_requested:
154
+ self.current_task.cancel()
155
+ try:
156
+ await self.current_task
157
+ except asyncio.CancelledError:
158
+ pass
159
+ return None
160
+
161
+ await asyncio.sleep(0.1) # Check every 100ms
162
+
163
+ # Get the result
164
+ response = await self.current_task
165
+ return response
166
+
167
+ except asyncio.CancelledError:
168
+ return None
169
+ finally:
170
+ self.current_task = None
171
+
172
+ def interrupt_current_task(self):
173
+ """Interrupt the current running task."""
174
+ if self.current_task and not self.current_task.done():
175
+ self.interrupt_requested = True
176
+ self.console.print("\n⚠️ [bold yellow]Task interruption requested...[/bold yellow]")
177
+
178
+ async def cleanup(self):
179
+ """Clean up resources."""
180
+ if self.mcp_loader:
181
+ try:
182
+ await self.mcp_loader.close()
183
+ except Exception as e:
184
+ if self.verbose:
185
+ self.console.print(f"[dim]Error during MCP cleanup: {e}[/dim]")
186
+
187
+ async def process_input(self, user_input: str):
188
+ """Process user input."""
189
+ user_input = user_input.strip()
190
+
191
+ if self.verbose:
192
+ self.console.print(f"[dim]Processing input: {user_input[:50]}{'...' if len(user_input) > 50 else ''}[/dim]")
193
+
194
+ # Check if it's a command (starts with /)
195
+ if user_input.startswith('/'):
196
+ await self.process_command(user_input)
197
+ return
198
+
199
+ # Process with agent
200
+ try:
201
+ with Progress(
202
+ SpinnerColumn(),
203
+ TextColumn("[progress.description]{task.description} (Ctrl+C to interrupt)"),
204
+ console=self.console,
205
+ ) as progress:
206
+ task = progress.add_task("🤖 Processing...", total=None)
207
+
208
+ response = await self.process_input_with_interrupt(user_input)
209
+
210
+ progress.update(task, completed=True)
211
+
212
+ if response is None:
213
+ # Task was cancelled
214
+ cancelled_panel = Panel(
215
+ "⚠️ [bold yellow]Task was interrupted![/bold yellow]",
216
+ title="[bold yellow]Interrupted[/bold yellow]",
217
+ border_style="yellow"
218
+ )
219
+ self.console.print(cancelled_panel)
220
+ return
221
+
222
+ # Display agent response with rich formatting
223
+ if "```" in response.answer:
224
+ agent_content = Markdown(response.answer)
225
+ else:
226
+ agent_content = response.answer
227
+
228
+ response_panel = Panel(
229
+ agent_content,
230
+ title="🤖 [bold green]Agent Response[/bold green]",
231
+ border_style="green"
232
+ )
233
+ self.console.print(response_panel)
234
+
235
+ if self.verbose:
236
+ self.console.print(f"[dim]Response length: {len(response.answer)} characters[/dim]")
237
+
238
+ except KeyboardInterrupt:
239
+ # Handle Ctrl+C during processing
240
+ self.interrupt_current_task()
241
+ cancelled_panel = Panel(
242
+ "⚠️ [bold yellow]Task interrupted by user![/bold yellow]",
243
+ title="[bold yellow]Interrupted[/bold yellow]",
244
+ border_style="yellow"
245
+ )
246
+ self.console.print(cancelled_panel)
247
+ except Exception as e:
248
+ error_panel = Panel(
249
+ f"❌ [bold red]Error: {e}[/bold red]",
250
+ title="[bold red]Error[/bold red]",
251
+ border_style="red"
252
+ )
253
+ self.console.print(error_panel)
254
+
255
+ if self.verbose:
256
+ import traceback
257
+ self.console.print(f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]")
258
+
259
+ async def process_command(self, command_input: str):
260
+ """Process a command input."""
261
+ # Remove the leading /
262
+ command_input = command_input[1:] if command_input.startswith('/') else command_input
263
+
264
+ # Split command and arguments
265
+ parts = command_input.split(' ', 1)
266
+ command_name = parts[0].lower()
267
+ args = parts[1] if len(parts) > 1 else ""
268
+
269
+ if self.verbose:
270
+ self.console.print(f"[dim]Executing command: {command_name} with args: {args}[/dim]")
271
+
272
+ # Get command class
273
+ command_class = command_registry.get_command(command_name)
274
+ if not command_class:
275
+ error_panel = Panel(
276
+ f"❌ [bold red]Unknown command: /{command_name}[/bold red]\n"
277
+ f"💡 [italic]Use '/help' to see available commands[/italic]",
278
+ title="[bold red]Error[/bold red]",
279
+ border_style="red"
280
+ )
281
+ self.console.print(error_panel)
282
+ return
283
+
284
+ # Create and execute command
285
+ try:
286
+ command_instance = command_class(self.console, self.agent)
287
+
288
+ # Special handling for quit command
289
+ if command_name in ["quit", "exit", "q", "bye"]:
290
+ command_instance._tui_instance = self
291
+
292
+ await command_instance.execute(args)
293
+
294
+ except Exception as e:
295
+ error_panel = Panel(
296
+ f"❌ [bold red]Error executing command /{command_name}: {e}[/bold red]",
297
+ title="[bold red]Command Error[/bold red]",
298
+ border_style="red"
299
+ )
300
+ self.console.print(error_panel)
301
+
302
+ if self.verbose:
303
+ import traceback
304
+ self.console.print(f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]")
305
+
306
+ def show_tools(self):
307
+ """Show available tools in a beautiful table."""
308
+ if not self.agent or not self.agent.tools:
309
+ self.console.print("❌ No tools available")
310
+ return
311
+
312
+ tools_table = Table(title="🛠️ Available Tools", show_header=True, header_style="bold magenta")
313
+ tools_table.add_column("Tool Name", style="cyan", no_wrap=True)
314
+ tools_table.add_column("Description", style="white")
315
+ tools_table.add_column("Source", style="yellow")
316
+ tools_table.add_column("Type", style="green")
317
+
318
+ # Separate MCP tools from built-in tools
319
+ mcp_tool_names = {tool.name for tool in self.mcp_tools} if self.mcp_tools else set()
320
+
321
+ for tool in self.agent.tools:
322
+ tool_type = "Read-only" if getattr(tool, 'readonly', False) else "Read-write"
323
+ source = "MCP" if tool.name in mcp_tool_names else "Built-in"
324
+
325
+ tools_table.add_row(
326
+ tool.name,
327
+ tool.description[:60] + "..." if len(tool.description) > 60 else tool.description,
328
+ source,
329
+ tool_type
330
+ )
331
+
332
+ self.console.print(tools_table)
333
+
334
+ # Show summary
335
+ total_tools = len(self.agent.tools)
336
+ mcp_count = len(self.mcp_tools) if self.mcp_tools else 0
337
+ builtin_count = total_tools - mcp_count
338
+
339
+ summary_text = f"[dim]Total: {total_tools} tools"
340
+ if mcp_count > 0:
341
+ summary_text += f" (Built-in: {builtin_count}, MCP: {mcp_count})"
342
+ summary_text += "[/dim]"
343
+
344
+ self.console.print(summary_text)
345
+
346
+ def show_history(self):
347
+ """Show conversation history in a beautiful format."""
348
+ if not self.agent:
349
+ return
350
+
351
+ history = self.agent.get_conversation_history()
352
+ if not history:
353
+ no_history_panel = Panel(
354
+ "📝 [italic]No conversation history yet.[/italic]",
355
+ title="[bold blue]History[/bold blue]",
356
+ border_style="blue"
357
+ )
358
+ self.console.print(no_history_panel)
359
+ return
360
+
361
+ history_panel = Panel(
362
+ f"📝 [bold blue]Conversation History ({len(history)} messages)[/bold blue]",
363
+ border_style="blue"
364
+ )
365
+ self.console.print(history_panel)
366
+
367
+ display_count = 5 if not self.verbose else 10
368
+ for i, entry in enumerate(history[-display_count:], 1):
369
+ # User message
370
+ user_panel = Panel(
371
+ entry['user_message'][:200] + "..." if len(entry['user_message']) > 200 else entry['user_message'],
372
+ title=f"👤 [bold cyan]You (Message {len(history)-display_count+i})[/bold cyan]",
373
+ border_style="cyan"
374
+ )
375
+ self.console.print(user_panel)
376
+
377
+ # Agent response
378
+ agent_response = entry['agent_response'][:200] + "..." if len(entry['agent_response']) > 200 else entry['agent_response']
379
+ agent_panel = Panel(
380
+ agent_response,
381
+ title="🤖 [bold green]Agent[/bold green]",
382
+ border_style="green"
383
+ )
384
+ self.console.print(agent_panel)
385
+ self.console.print() # Add spacing
386
+
387
+ async def run(self):
388
+ """Run the CLI."""
389
+ # Welcome banner
390
+ welcome_panel = Panel(
391
+ "🚀 [bold blue]MinionCodeAgent CLI[/bold blue]\n"
392
+ "💡 [italic]Use '/help' for commands or just chat with the agent![/italic]\n"
393
+ "⚠️ [italic]Press Ctrl+C during processing to interrupt tasks[/italic]\n"
394
+ "🛑 [italic]Type '/quit' to exit[/italic]",
395
+ title="[bold magenta]Welcome[/bold magenta]",
396
+ border_style="magenta"
397
+ )
398
+ self.console.print(welcome_panel)
399
+
400
+ await self.setup()
401
+
402
+ while self.running:
403
+ try:
404
+ # Use rich prompt for better input experience
405
+ user_input = Prompt.ask(
406
+ "\n[bold cyan]👤 You[/bold cyan]",
407
+ console=self.console
408
+ ).strip()
409
+
410
+ if user_input:
411
+ await self.process_input(user_input)
412
+
413
+ except (EOFError, KeyboardInterrupt):
414
+ # Handle Ctrl+C at input prompt
415
+ if self.current_task and not self.current_task.done():
416
+ # If there's a running task, interrupt it
417
+ self.interrupt_current_task()
418
+ else:
419
+ # If no running task, exit
420
+ goodbye_panel = Panel(
421
+ "\n👋 [bold yellow]Goodbye![/bold yellow]",
422
+ title="[bold red]Exit[/bold red]",
423
+ border_style="red"
424
+ )
425
+ self.console.print(goodbye_panel)
426
+ break
427
+
428
+ # Cleanup resources
429
+ await self.cleanup()
430
+
431
+
432
+ @app.command()
433
+ def main(
434
+ dir: Optional[str] = typer.Option(
435
+ None,
436
+ "--dir",
437
+ "-d",
438
+ help="🗂️ Change to specified directory before starting"
439
+ ),
440
+ verbose: bool = typer.Option(
441
+ False,
442
+ "--verbose",
443
+ "-v",
444
+ help="🔍 Enable verbose output with additional debugging information"
445
+ ),
446
+ config: Optional[str] = typer.Option(
447
+ None,
448
+ "--config",
449
+ "-c",
450
+ help="🔌 Path to MCP configuration file (JSON format)"
451
+ )
452
+ ):
453
+ """
454
+ 🤖 Start the MinionCodeAgent CLI interface
455
+
456
+ An AI-powered code assistant with task interruption support and MCP tools integration.
457
+ """
458
+ console = Console()
459
+
460
+ # Change directory if specified
461
+ if dir:
462
+ try:
463
+ target_dir = Path(dir).resolve()
464
+ if not target_dir.exists():
465
+ console.print(f"❌ [bold red]Directory does not exist: {dir}[/bold red]")
466
+ raise typer.Exit(1)
467
+ if not target_dir.is_dir():
468
+ console.print(f"❌ [bold red]Path is not a directory: {dir}[/bold red]")
469
+ raise typer.Exit(1)
470
+
471
+ os.chdir(target_dir)
472
+ if verbose:
473
+ console.print(f"📁 [bold green]Changed to directory: {target_dir}[/bold green]")
474
+ except Exception as e:
475
+ console.print(f"❌ [bold red]Failed to change directory: {e}[/bold red]")
476
+ raise typer.Exit(1)
477
+
478
+ # Validate MCP config if provided
479
+ mcp_config_path = None
480
+ if config:
481
+ mcp_config_path = Path(config).resolve()
482
+ if not mcp_config_path.exists():
483
+ console.print(f"❌ [bold red]MCP config file does not exist: {config}[/bold red]")
484
+ raise typer.Exit(1)
485
+ if not mcp_config_path.is_file():
486
+ console.print(f"❌ [bold red]MCP config path is not a file: {config}[/bold red]")
487
+ raise typer.Exit(1)
488
+
489
+ if verbose:
490
+ console.print(f"🔌 [bold green]Using MCP config: {mcp_config_path}[/bold green]")
491
+
492
+ # Create and run CLI
493
+ cli = InterruptibleCLI(verbose=verbose, mcp_config=mcp_config_path)
494
+
495
+ try:
496
+ asyncio.run(cli.run())
497
+ except KeyboardInterrupt:
498
+ console.print("\n👋 [bold yellow]Goodbye![/bold yellow]")
499
+
500
+
501
+ if __name__ == "__main__":
502
+ app()
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Command system for MinionCode TUI
5
+
6
+ This module provides a command system similar to Claude Code or Gemini CLI,
7
+ where commands are prefixed with '/' and each command is implemented in a separate file.
8
+ """
9
+
10
+ import importlib
11
+ import pkgutil
12
+ from typing import Dict, Type, Optional
13
+ from abc import ABC, abstractmethod
14
+
15
+
16
+ class BaseCommand(ABC):
17
+ """Base class for all commands."""
18
+
19
+ name: str = ""
20
+ description: str = ""
21
+ usage: str = ""
22
+ aliases: list = []
23
+
24
+ def __init__(self, console, agent=None):
25
+ self.console = console
26
+ self.agent = agent
27
+
28
+ @abstractmethod
29
+ async def execute(self, args: str) -> None:
30
+ """Execute the command with given arguments."""
31
+ pass
32
+
33
+ def get_help(self) -> str:
34
+ """Get help text for this command."""
35
+ return f"**/{self.name}** - {self.description}\n\nUsage: {self.usage}"
36
+
37
+
38
+ class CommandRegistry:
39
+ """Registry for managing commands."""
40
+
41
+ def __init__(self):
42
+ self.commands: Dict[str, Type[BaseCommand]] = {}
43
+ self._load_commands()
44
+
45
+ def _load_commands(self):
46
+ """Dynamically load all command modules."""
47
+ import os
48
+ commands_dir = os.path.dirname(__file__)
49
+
50
+ for filename in os.listdir(commands_dir):
51
+ if filename.endswith('_command.py') and not filename.startswith('_'):
52
+ modname = filename[:-3] # Remove .py extension
53
+ try:
54
+ module = importlib.import_module(f'minion_code.commands.{modname}')
55
+
56
+ # Look for command classes in the module
57
+ for attr_name in dir(module):
58
+ attr = getattr(module, attr_name)
59
+ if (isinstance(attr, type) and
60
+ issubclass(attr, BaseCommand) and
61
+ attr != BaseCommand and
62
+ hasattr(attr, 'name') and attr.name):
63
+
64
+ self.commands[attr.name] = attr
65
+
66
+ # Register aliases
67
+ for alias in getattr(attr, 'aliases', []):
68
+ self.commands[alias] = attr
69
+
70
+ except ImportError as e:
71
+ print(f"Failed to load command module {modname}: {e}")
72
+
73
+ def get_command(self, name: str) -> Optional[Type[BaseCommand]]:
74
+ """Get a command class by name."""
75
+ return self.commands.get(name)
76
+
77
+ def list_commands(self) -> Dict[str, Type[BaseCommand]]:
78
+ """List all available commands."""
79
+ # Return only primary commands (not aliases)
80
+ return {name: cmd for name, cmd in self.commands.items()
81
+ if cmd.name == name}
82
+
83
+ def reload_commands(self):
84
+ """Reload all commands."""
85
+ self.commands.clear()
86
+ self._load_commands()
87
+
88
+
89
+ # Global command registry
90
+ command_registry = CommandRegistry()
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Clear command - Clear conversation history
5
+ """
6
+
7
+ from rich.panel import Panel
8
+ from rich.prompt import Confirm
9
+ from minion_code.commands import BaseCommand
10
+
11
+
12
+ class ClearCommand(BaseCommand):
13
+ """Clear conversation history."""
14
+
15
+ name = "clear"
16
+ description = "Clear the conversation history"
17
+ usage = "/clear [--force]"
18
+ aliases = ["c", "reset"]
19
+
20
+ async def execute(self, args: str) -> None:
21
+ """Execute the clear command."""
22
+ if not self.agent:
23
+ error_panel = Panel(
24
+ "❌ [bold red]Agent not initialized[/bold red]",
25
+ title="[bold red]Error[/bold red]",
26
+ border_style="red"
27
+ )
28
+ self.console.print(error_panel)
29
+ return
30
+
31
+ force = "--force" in args or "-f" in args
32
+
33
+ history = self.agent.get_conversation_history()
34
+ if not history:
35
+ no_history_panel = Panel(
36
+ "📝 [italic]No conversation history to clear.[/italic]",
37
+ title="[bold blue]Info[/bold blue]",
38
+ border_style="blue"
39
+ )
40
+ self.console.print(no_history_panel)
41
+ return
42
+
43
+ # Confirm before clearing unless --force is used
44
+ if not force:
45
+ confirm_panel = Panel(
46
+ f"⚠️ [bold yellow]This will clear {len(history)} messages from history.[/bold yellow]\n"
47
+ "This action cannot be undone.",
48
+ title="[bold yellow]Confirm Clear[/bold yellow]",
49
+ border_style="yellow"
50
+ )
51
+ self.console.print(confirm_panel)
52
+
53
+ if not Confirm.ask("Are you sure you want to clear the history?", console=self.console):
54
+ cancel_panel = Panel(
55
+ "❌ [bold blue]Clear operation cancelled.[/bold blue]",
56
+ title="[bold blue]Cancelled[/bold blue]",
57
+ border_style="blue"
58
+ )
59
+ self.console.print(cancel_panel)
60
+ return
61
+
62
+ # Clear the history
63
+ self.agent.clear_conversation_history()
64
+
65
+ success_panel = Panel(
66
+ f"🗑️ [bold green]Successfully cleared {len(history)} messages from history.[/bold green]",
67
+ title="[bold green]History Cleared[/bold green]",
68
+ border_style="green"
69
+ )
70
+ self.console.print(success_panel)