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.
- examples/advance_tui.py +508 -0
- examples/agent_with_todos.py +165 -0
- examples/file_freshness_example.py +97 -0
- examples/file_watching_example.py +110 -0
- examples/interruptible_tui.py +5 -0
- examples/message_response_children_demo.py +226 -0
- examples/rich_example.py +4 -0
- examples/simple_file_watching.py +57 -0
- examples/simple_tui.py +267 -0
- examples/simple_usage.py +69 -0
- minion_code/__init__.py +16 -0
- minion_code/agents/__init__.py +11 -0
- minion_code/agents/code_agent.py +320 -0
- minion_code/cli.py +502 -0
- minion_code/commands/__init__.py +90 -0
- minion_code/commands/clear_command.py +70 -0
- minion_code/commands/help_command.py +90 -0
- minion_code/commands/history_command.py +104 -0
- minion_code/commands/quit_command.py +32 -0
- minion_code/commands/status_command.py +115 -0
- minion_code/commands/tools_command.py +86 -0
- minion_code/commands/version_command.py +104 -0
- minion_code/components/Message.py +304 -0
- minion_code/components/MessageResponse.py +188 -0
- minion_code/components/PromptInput.py +534 -0
- minion_code/components/__init__.py +29 -0
- minion_code/screens/REPL.py +925 -0
- minion_code/screens/__init__.py +4 -0
- minion_code/services/__init__.py +50 -0
- minion_code/services/event_system.py +108 -0
- minion_code/services/file_freshness_service.py +582 -0
- minion_code/tools/__init__.py +69 -0
- minion_code/tools/bash_tool.py +58 -0
- minion_code/tools/file_edit_tool.py +238 -0
- minion_code/tools/file_read_tool.py +73 -0
- minion_code/tools/file_write_tool.py +36 -0
- minion_code/tools/glob_tool.py +58 -0
- minion_code/tools/grep_tool.py +105 -0
- minion_code/tools/ls_tool.py +65 -0
- minion_code/tools/multi_edit_tool.py +271 -0
- minion_code/tools/python_interpreter_tool.py +105 -0
- minion_code/tools/todo_read_tool.py +100 -0
- minion_code/tools/todo_write_tool.py +234 -0
- minion_code/tools/user_input_tool.py +53 -0
- minion_code/types.py +88 -0
- minion_code/utils/__init__.py +44 -0
- minion_code/utils/mcp_loader.py +211 -0
- minion_code/utils/todo_file_utils.py +110 -0
- minion_code/utils/todo_storage.py +149 -0
- minion_code-0.1.0.dist-info/METADATA +350 -0
- minion_code-0.1.0.dist-info/RECORD +59 -0
- minion_code-0.1.0.dist-info/WHEEL +5 -0
- minion_code-0.1.0.dist-info/entry_points.txt +4 -0
- minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
- minion_code-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_basic.py +20 -0
- tests/test_readonly_tools.py +102 -0
- 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)
|