minion-code 0.1.0__py3-none-any.whl → 0.1.1__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/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.1.dist-info/METADATA +475 -0
- minion_code-0.1.1.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
- minion_code-0.1.1.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
examples/interruptible_tui.py
CHANGED
|
@@ -1,5 +1,923 @@
|
|
|
1
|
-
|
|
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
|
+
from minion_code.type_defs import InputMode
|
|
31
|
+
from minion_code.adapters.rich_adapter import RichOutputAdapter
|
|
32
|
+
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
name="minion-code",
|
|
35
|
+
help="🤖 MinionCodeAgent CLI - An AI-powered code assistant",
|
|
36
|
+
add_completion=False,
|
|
37
|
+
rich_markup_mode="rich",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InterruptibleCLI:
|
|
42
|
+
"""CLI with task interruption support using standard input."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, verbose: bool = False, mcp_config: Optional[Path] = None):
|
|
45
|
+
self.agent = None
|
|
46
|
+
self.running = True
|
|
47
|
+
self.console = Console()
|
|
48
|
+
self.verbose = verbose
|
|
49
|
+
self.mcp_config = mcp_config
|
|
50
|
+
self.mcp_tools = []
|
|
51
|
+
self.mcp_loader = None
|
|
52
|
+
self.current_task = None
|
|
53
|
+
self.task_cancelled = False
|
|
54
|
+
self.interrupt_requested = False
|
|
55
|
+
|
|
56
|
+
# Add mode support
|
|
57
|
+
self.current_mode = InputMode.PROMPT
|
|
58
|
+
|
|
59
|
+
async def setup(self):
|
|
60
|
+
"""Setup the agent."""
|
|
61
|
+
with Progress(
|
|
62
|
+
SpinnerColumn(),
|
|
63
|
+
TextColumn("[progress.description]{task.description}"),
|
|
64
|
+
console=self.console,
|
|
65
|
+
) as progress:
|
|
66
|
+
# Load MCP tools - auto-discover if not explicitly provided
|
|
67
|
+
mcp_task = progress.add_task("🔌 Loading MCP tools...", total=None)
|
|
68
|
+
try:
|
|
69
|
+
# MCPToolsLoader will auto-discover config if mcp_config is None
|
|
70
|
+
self.mcp_loader = MCPToolsLoader(self.mcp_config, auto_discover=True)
|
|
71
|
+
|
|
72
|
+
if self.mcp_loader.config_path:
|
|
73
|
+
if self.verbose:
|
|
74
|
+
self.console.print(
|
|
75
|
+
f"[dim]Using MCP config: {self.mcp_loader.config_path}[/dim]"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.mcp_loader.load_config()
|
|
79
|
+
self.mcp_tools = await self.mcp_loader.load_all_tools()
|
|
80
|
+
|
|
81
|
+
if self.mcp_tools:
|
|
82
|
+
self.console.print(
|
|
83
|
+
f"✅ Loaded {len(self.mcp_tools)} MCP tools from {self.mcp_loader.config_path}"
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
server_info = self.mcp_loader.get_server_info()
|
|
87
|
+
if server_info:
|
|
88
|
+
self.console.print(
|
|
89
|
+
f"📋 Found {len(server_info)} MCP server(s) configured"
|
|
90
|
+
)
|
|
91
|
+
for name, info in server_info.items():
|
|
92
|
+
status = "disabled" if info["disabled"] else "enabled"
|
|
93
|
+
self.console.print(
|
|
94
|
+
f" - {name}: {info['command']} ({status})"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
self.console.print("⚠️ No MCP servers found in config")
|
|
98
|
+
else:
|
|
99
|
+
if self.verbose:
|
|
100
|
+
self.console.print(
|
|
101
|
+
"[dim]No MCP config found in standard locations[/dim]"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
progress.update(mcp_task, completed=True)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
self.console.print(f"❌ Failed to load MCP tools: {e}")
|
|
107
|
+
if self.verbose:
|
|
108
|
+
import traceback
|
|
109
|
+
|
|
110
|
+
self.console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
111
|
+
progress.update(mcp_task, completed=True)
|
|
112
|
+
|
|
113
|
+
agent_task = progress.add_task(
|
|
114
|
+
"🔧 Setting up MinionCodeAgent...", total=None
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.agent = await MinionCodeAgent.create(
|
|
118
|
+
name="CLI Code Assistant",
|
|
119
|
+
llm="sonnet", # 使用更稳定的模型配置
|
|
120
|
+
additional_tools=self.mcp_tools if self.mcp_tools else None,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
progress.update(agent_task, completed=True)
|
|
124
|
+
|
|
125
|
+
# Show setup summary
|
|
126
|
+
total_tools = len(self.agent.tools)
|
|
127
|
+
mcp_count = len(self.mcp_tools)
|
|
128
|
+
builtin_count = total_tools - mcp_count
|
|
129
|
+
|
|
130
|
+
summary_text = (
|
|
131
|
+
f"✅ Agent ready with [bold green]{total_tools}[/bold green] tools!"
|
|
132
|
+
)
|
|
133
|
+
if mcp_count > 0:
|
|
134
|
+
summary_text += f"\n🔌 MCP tools: [bold cyan]{mcp_count}[/bold cyan]"
|
|
135
|
+
summary_text += (
|
|
136
|
+
f"\n🛠️ Built-in tools: [bold blue]{builtin_count}[/bold blue]"
|
|
137
|
+
)
|
|
138
|
+
summary_text += f"\n⚠️ [bold yellow]Press Ctrl+C during processing to interrupt tasks[/bold yellow]"
|
|
139
|
+
|
|
140
|
+
success_panel = Panel(
|
|
141
|
+
summary_text,
|
|
142
|
+
title="[bold green]Setup Complete[/bold green]",
|
|
143
|
+
border_style="green",
|
|
144
|
+
)
|
|
145
|
+
self.console.print(success_panel)
|
|
146
|
+
|
|
147
|
+
if self.verbose:
|
|
148
|
+
self.console.print(f"[dim]Working directory: {os.getcwd()}[/dim]")
|
|
149
|
+
|
|
150
|
+
def show_help(self):
|
|
151
|
+
"""Show help information."""
|
|
152
|
+
help_table = Table(
|
|
153
|
+
title="📚 MinionCode CLI Help", show_header=True, header_style="bold blue"
|
|
154
|
+
)
|
|
155
|
+
help_table.add_column("Command/Key", style="cyan", no_wrap=True)
|
|
156
|
+
help_table.add_column("Description", style="white")
|
|
157
|
+
|
|
158
|
+
help_table.add_row("help", "Show this help")
|
|
159
|
+
help_table.add_row("tools", "List available tools")
|
|
160
|
+
help_table.add_row("history", "Show conversation history")
|
|
161
|
+
help_table.add_row("clear", "Clear history")
|
|
162
|
+
help_table.add_row("quit", "Exit")
|
|
163
|
+
help_table.add_row("Ctrl+C", "Interrupt current task or exit")
|
|
164
|
+
|
|
165
|
+
# Add mode information
|
|
166
|
+
help_table.add_row("", "") # Separator
|
|
167
|
+
help_table.add_row("[bold]Input Modes:[/bold]", "")
|
|
168
|
+
help_table.add_row("> [text]", "Prompt mode - Chat with AI assistant")
|
|
169
|
+
help_table.add_row("! [command]", "Bash mode - Execute shell commands")
|
|
170
|
+
help_table.add_row("# [note]", "Koding mode - Add notes to AGENTS.md")
|
|
171
|
+
help_table.add_row("", "") # Separator
|
|
172
|
+
help_table.add_row("[bold]Koding Mode Types:[/bold]", "")
|
|
173
|
+
help_table.add_row("# [simple note]", "Direct note (simple write)")
|
|
174
|
+
help_table.add_row("# put/create/generate...", "AI processing with query_quick")
|
|
175
|
+
|
|
176
|
+
self.console.print(help_table)
|
|
177
|
+
self.console.print(
|
|
178
|
+
"\n💡 [italic]Just type your message to chat with the AI agent![/italic]"
|
|
179
|
+
)
|
|
180
|
+
self.console.print(
|
|
181
|
+
"⚠️ [italic]During task processing, press Ctrl+C to interrupt the current task[/italic]"
|
|
182
|
+
)
|
|
183
|
+
self.console.print(
|
|
184
|
+
"🔄 [italic]Use prefixes !, # to switch modes, or just type normally for prompt mode[/italic]"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async def process_input_with_interrupt(self, user_input: str):
|
|
188
|
+
"""Process user input with interrupt support."""
|
|
189
|
+
self.task_cancelled = False
|
|
190
|
+
self.interrupt_requested = False
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# Create the actual processing task
|
|
194
|
+
async def processing_task():
|
|
195
|
+
response = await self.agent.run_async(user_input)
|
|
196
|
+
return response
|
|
197
|
+
|
|
198
|
+
# Start the task
|
|
199
|
+
self.current_task = asyncio.create_task(processing_task())
|
|
200
|
+
|
|
201
|
+
# Monitor for cancellation while task runs
|
|
202
|
+
while not self.current_task.done():
|
|
203
|
+
if self.interrupt_requested:
|
|
204
|
+
self.current_task.cancel()
|
|
205
|
+
try:
|
|
206
|
+
await self.current_task
|
|
207
|
+
except asyncio.CancelledError:
|
|
208
|
+
pass
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
await asyncio.sleep(0.1) # Check every 100ms
|
|
212
|
+
|
|
213
|
+
# Get the result
|
|
214
|
+
response = await self.current_task
|
|
215
|
+
return response
|
|
216
|
+
|
|
217
|
+
except asyncio.CancelledError:
|
|
218
|
+
return None
|
|
219
|
+
finally:
|
|
220
|
+
self.current_task = None
|
|
221
|
+
|
|
222
|
+
def interrupt_current_task(self):
|
|
223
|
+
"""Interrupt the current running task."""
|
|
224
|
+
if self.current_task and not self.current_task.done():
|
|
225
|
+
self.interrupt_requested = True
|
|
226
|
+
self.console.print(
|
|
227
|
+
"\n⚠️ [bold yellow]Task interruption requested...[/bold yellow]"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def cleanup(self):
|
|
231
|
+
"""Clean up resources."""
|
|
232
|
+
if self.mcp_loader:
|
|
233
|
+
try:
|
|
234
|
+
await self.mcp_loader.close()
|
|
235
|
+
except Exception as e:
|
|
236
|
+
if self.verbose:
|
|
237
|
+
self.console.print(f"[dim]Error during MCP cleanup: {e}[/dim]")
|
|
238
|
+
|
|
239
|
+
def _detect_and_set_mode(self, user_input: str) -> tuple[InputMode, str]:
|
|
240
|
+
"""Detect input mode and return mode and cleaned input."""
|
|
241
|
+
if user_input.startswith("!"):
|
|
242
|
+
self.current_mode = InputMode.BASH
|
|
243
|
+
return InputMode.BASH, user_input[1:].strip()
|
|
244
|
+
elif user_input.startswith("#"):
|
|
245
|
+
self.current_mode = InputMode.KODING
|
|
246
|
+
return InputMode.KODING, user_input[1:].strip()
|
|
247
|
+
else:
|
|
248
|
+
self.current_mode = InputMode.PROMPT
|
|
249
|
+
return InputMode.PROMPT, user_input
|
|
250
|
+
|
|
251
|
+
def _get_mode_indicator(self, mode: InputMode) -> str:
|
|
252
|
+
"""Get colored mode indicator for display."""
|
|
253
|
+
if mode == InputMode.BASH:
|
|
254
|
+
return "[bold yellow]![/bold yellow]"
|
|
255
|
+
elif mode == InputMode.KODING:
|
|
256
|
+
return "[bold cyan]#[/bold cyan]"
|
|
257
|
+
else:
|
|
258
|
+
return "[bold green]>[/bold green]"
|
|
259
|
+
|
|
260
|
+
async def _handle_bash_mode(self, command: str):
|
|
261
|
+
"""Handle bash mode input."""
|
|
262
|
+
if not command:
|
|
263
|
+
self.console.print("❌ [bold red]Empty bash command[/bold red]")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
import subprocess
|
|
268
|
+
|
|
269
|
+
# Show what command is being executed
|
|
270
|
+
command_panel = Panel(
|
|
271
|
+
f"[bold white]{command}[/bold white]",
|
|
272
|
+
title=f"{self._get_mode_indicator(InputMode.BASH)} [bold yellow]Bash Command[/bold yellow]",
|
|
273
|
+
border_style="yellow",
|
|
274
|
+
)
|
|
275
|
+
self.console.print(command_panel)
|
|
276
|
+
|
|
277
|
+
# Execute command with timeout
|
|
278
|
+
result = subprocess.run(
|
|
279
|
+
command, shell=True, capture_output=True, text=True, timeout=30
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Format output
|
|
283
|
+
if result.returncode == 0:
|
|
284
|
+
output = (
|
|
285
|
+
result.stdout.strip()
|
|
286
|
+
if result.stdout
|
|
287
|
+
else "Command executed successfully"
|
|
288
|
+
)
|
|
289
|
+
if output:
|
|
290
|
+
output_panel = Panel(
|
|
291
|
+
output,
|
|
292
|
+
title="✅ [bold green]Command Output[/bold green]",
|
|
293
|
+
border_style="green",
|
|
294
|
+
)
|
|
295
|
+
self.console.print(output_panel)
|
|
296
|
+
else:
|
|
297
|
+
error_output = (
|
|
298
|
+
result.stderr.strip()
|
|
299
|
+
if result.stderr
|
|
300
|
+
else f"Command failed with exit code {result.returncode}"
|
|
301
|
+
)
|
|
302
|
+
error_panel = Panel(
|
|
303
|
+
error_output,
|
|
304
|
+
title="❌ [bold red]Command Error[/bold red]",
|
|
305
|
+
border_style="red",
|
|
306
|
+
)
|
|
307
|
+
self.console.print(error_panel)
|
|
308
|
+
|
|
309
|
+
except subprocess.TimeoutExpired:
|
|
310
|
+
timeout_panel = Panel(
|
|
311
|
+
"⏰ [bold yellow]Command timed out after 30 seconds[/bold yellow]",
|
|
312
|
+
title="[bold yellow]Timeout[/bold yellow]",
|
|
313
|
+
border_style="yellow",
|
|
314
|
+
)
|
|
315
|
+
self.console.print(timeout_panel)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
error_panel = Panel(
|
|
318
|
+
f"❌ [bold red]Error executing command: {e}[/bold red]",
|
|
319
|
+
title="[bold red]Execution Error[/bold red]",
|
|
320
|
+
border_style="red",
|
|
321
|
+
)
|
|
322
|
+
self.console.print(error_panel)
|
|
323
|
+
|
|
324
|
+
async def _handle_koding_mode(self, note_content: str):
|
|
325
|
+
"""Handle koding mode input - consistent with REPL logic."""
|
|
326
|
+
if not note_content:
|
|
327
|
+
self.console.print("❌ [bold red]Empty note content[/bold red]")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# Show what note is being processed
|
|
332
|
+
note_panel = Panel(
|
|
333
|
+
f"[bold white]{note_content}[/bold white]",
|
|
334
|
+
title=f"{self._get_mode_indicator(InputMode.KODING)} [bold cyan]Processing Koding Request[/bold cyan]",
|
|
335
|
+
border_style="cyan",
|
|
336
|
+
)
|
|
337
|
+
self.console.print(note_panel)
|
|
338
|
+
|
|
339
|
+
# Check if this is an action prompt (put, create, generate, etc.)
|
|
340
|
+
# Add safety check to prevent NoneType iteration error
|
|
341
|
+
action_words = ["put", "create", "generate", "write", "give", "provide"]
|
|
342
|
+
note_lower = note_content.lower() if note_content else ""
|
|
343
|
+
is_action_request = any(word in note_lower for word in action_words)
|
|
344
|
+
|
|
345
|
+
if is_action_request:
|
|
346
|
+
# Handle as AI request using query_quick for lightweight processing
|
|
347
|
+
await self._handle_koding_ai_request(note_content)
|
|
348
|
+
else:
|
|
349
|
+
# Handle as direct note to AGENTS.md (simple write)
|
|
350
|
+
await self._handle_koding_note(note_content)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
error_panel = Panel(
|
|
354
|
+
f"❌ [bold red]Error processing koding request: {e}[/bold red]",
|
|
355
|
+
title="[bold red]Koding Error[/bold red]",
|
|
356
|
+
border_style="red",
|
|
357
|
+
)
|
|
358
|
+
self.console.print(error_panel)
|
|
359
|
+
|
|
360
|
+
async def _handle_koding_ai_request(self, content: str):
|
|
361
|
+
"""Handle AI request for koding mode using query_quick for lightweight processing."""
|
|
362
|
+
if not self.agent:
|
|
363
|
+
self.console.print(
|
|
364
|
+
"❌ [bold red]Agent not available for AI requests[/bold red]"
|
|
365
|
+
)
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
# Import query_quick for lightweight AI processing
|
|
370
|
+
from minion_code.agents.code_agent import query_quick
|
|
371
|
+
|
|
372
|
+
# Show processing indicator
|
|
373
|
+
processing_panel = Panel(
|
|
374
|
+
"🤖 [italic]Processing AI request with query_quick...[/italic]",
|
|
375
|
+
title="[bold cyan]Processing[/bold cyan]",
|
|
376
|
+
border_style="cyan",
|
|
377
|
+
)
|
|
378
|
+
self.console.print(processing_panel)
|
|
379
|
+
|
|
380
|
+
# Create system prompt for AI content generation
|
|
381
|
+
system_prompt = [
|
|
382
|
+
"The user is using Koding mode. Format your response as a comprehensive,",
|
|
383
|
+
"well-structured document suitable for adding to AGENTS.md. Use proper",
|
|
384
|
+
"markdown formatting with headings, lists, code blocks, etc.",
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
# Use query_quick for lightweight AI processing
|
|
388
|
+
result = await query_quick(
|
|
389
|
+
agent=self.agent,
|
|
390
|
+
user_prompt=content,
|
|
391
|
+
system_prompt=system_prompt,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Extract formatted content
|
|
395
|
+
if isinstance(result, str):
|
|
396
|
+
formatted_content = result
|
|
397
|
+
else:
|
|
398
|
+
formatted_content = str(result)
|
|
399
|
+
|
|
400
|
+
# Add timestamp if not already present
|
|
401
|
+
import time
|
|
402
|
+
|
|
403
|
+
timestamp = time.strftime("%m/%d/%Y, %I:%M:%S %p")
|
|
404
|
+
if "_Added on" not in formatted_content:
|
|
405
|
+
formatted_content += f"\n\n_Added on {timestamp}_"
|
|
406
|
+
|
|
407
|
+
# Write to AGENTS.md
|
|
408
|
+
agents_md_path = Path("AGENTS.md")
|
|
409
|
+
|
|
410
|
+
# Create file if it doesn't exist
|
|
411
|
+
if not agents_md_path.exists():
|
|
412
|
+
with open(agents_md_path, "w", encoding="utf-8") as f:
|
|
413
|
+
f.write("# Agent Development Guidelines\n\n")
|
|
414
|
+
|
|
415
|
+
# Append the formatted content
|
|
416
|
+
with open(agents_md_path, "a", encoding="utf-8") as f:
|
|
417
|
+
f.write(f"\n\n{formatted_content}\n")
|
|
418
|
+
|
|
419
|
+
success_panel = Panel(
|
|
420
|
+
f"✅ [bold green]AI-generated content added to AGENTS.md[/bold green]\n"
|
|
421
|
+
f"📝 [italic]{len(formatted_content)} characters written[/italic]",
|
|
422
|
+
title="[bold green]Success[/bold green]",
|
|
423
|
+
border_style="green",
|
|
424
|
+
)
|
|
425
|
+
self.console.print(success_panel)
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
error_panel = Panel(
|
|
429
|
+
f"❌ [bold red]Error processing AI request: {e}[/bold red]",
|
|
430
|
+
title="[bold red]AI Error[/bold red]",
|
|
431
|
+
border_style="red",
|
|
432
|
+
)
|
|
433
|
+
self.console.print(error_panel)
|
|
434
|
+
|
|
435
|
+
async def _handle_koding_note(self, content: str):
|
|
436
|
+
"""Handle direct note to AGENTS.md - simple write without AI processing."""
|
|
437
|
+
try:
|
|
438
|
+
# Show what note is being added
|
|
439
|
+
note_panel = Panel(
|
|
440
|
+
f"[bold white]{content}[/bold white]",
|
|
441
|
+
title=f"{self._get_mode_indicator(InputMode.KODING)} [bold cyan]Adding Direct Note[/bold cyan]",
|
|
442
|
+
border_style="cyan",
|
|
443
|
+
)
|
|
444
|
+
self.console.print(note_panel)
|
|
445
|
+
|
|
446
|
+
# Simple direct write to AGENTS.md
|
|
447
|
+
import time
|
|
448
|
+
|
|
449
|
+
timestamp = time.strftime("%m/%d/%Y, %I:%M:%S %p")
|
|
450
|
+
formatted_content = f"# {content}\n\n_Added on {timestamp}_"
|
|
451
|
+
|
|
452
|
+
agents_md_path = Path("AGENTS.md")
|
|
453
|
+
|
|
454
|
+
# Create file if it doesn't exist
|
|
455
|
+
if not agents_md_path.exists():
|
|
456
|
+
with open(agents_md_path, "w", encoding="utf-8") as f:
|
|
457
|
+
f.write("# Agent Development Guidelines\n\n")
|
|
458
|
+
|
|
459
|
+
# Append the content
|
|
460
|
+
with open(agents_md_path, "a", encoding="utf-8") as f:
|
|
461
|
+
f.write(f"\n\n{formatted_content}\n")
|
|
462
|
+
|
|
463
|
+
success_panel = Panel(
|
|
464
|
+
f"✅ [bold green]Direct note added to AGENTS.md[/bold green]\n"
|
|
465
|
+
f"📝 [italic]{len(formatted_content)} characters written[/italic]",
|
|
466
|
+
title="[bold green]Success[/bold green]",
|
|
467
|
+
border_style="green",
|
|
468
|
+
)
|
|
469
|
+
self.console.print(success_panel)
|
|
470
|
+
|
|
471
|
+
except Exception as e:
|
|
472
|
+
error_panel = Panel(
|
|
473
|
+
f"❌ [bold red]Error writing direct note: {e}[/bold red]",
|
|
474
|
+
title="[bold red]File Error[/bold red]",
|
|
475
|
+
border_style="red",
|
|
476
|
+
)
|
|
477
|
+
self.console.print(error_panel)
|
|
478
|
+
|
|
479
|
+
async def _write_simple_note(self, content: str):
|
|
480
|
+
"""Write a simple formatted note to AGENTS.md as fallback."""
|
|
481
|
+
try:
|
|
482
|
+
import time
|
|
483
|
+
|
|
484
|
+
timestamp = time.strftime("%m/%d/%Y, %I:%M:%S %p")
|
|
485
|
+
formatted_content = f"# {content}\n\n_Added on {timestamp}_"
|
|
486
|
+
|
|
487
|
+
agents_md_path = Path("AGENTS.md")
|
|
488
|
+
|
|
489
|
+
# Create file if it doesn't exist
|
|
490
|
+
if not agents_md_path.exists():
|
|
491
|
+
with open(agents_md_path, "w", encoding="utf-8") as f:
|
|
492
|
+
f.write("# Agent Development Guidelines\n\n")
|
|
493
|
+
|
|
494
|
+
# Append the content
|
|
495
|
+
with open(agents_md_path, "a", encoding="utf-8") as f:
|
|
496
|
+
f.write(f"\n\n{formatted_content}\n")
|
|
497
|
+
|
|
498
|
+
success_panel = Panel(
|
|
499
|
+
f"✅ [bold green]Simple note added to AGENTS.md[/bold green]\n"
|
|
500
|
+
f"📝 [italic]{len(formatted_content)} characters written[/italic]",
|
|
501
|
+
title="[bold green]Success[/bold green]",
|
|
502
|
+
border_style="green",
|
|
503
|
+
)
|
|
504
|
+
self.console.print(success_panel)
|
|
505
|
+
|
|
506
|
+
except Exception as e:
|
|
507
|
+
error_panel = Panel(
|
|
508
|
+
f"❌ [bold red]Error writing simple note: {e}[/bold red]",
|
|
509
|
+
title="[bold red]File Error[/bold red]",
|
|
510
|
+
border_style="red",
|
|
511
|
+
)
|
|
512
|
+
self.console.print(error_panel)
|
|
513
|
+
|
|
514
|
+
async def process_input(self, user_input: str):
|
|
515
|
+
"""Process user input with mode detection."""
|
|
516
|
+
user_input = user_input.strip()
|
|
517
|
+
|
|
518
|
+
if self.verbose:
|
|
519
|
+
self.console.print(
|
|
520
|
+
f"[dim]Processing input: {user_input[:50]}{'...' if len(user_input) > 50 else ''}[/dim]"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Check if it's a command (starts with /)
|
|
524
|
+
if user_input.startswith("/"):
|
|
525
|
+
await self.process_command(user_input)
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# Detect mode and get cleaned input
|
|
529
|
+
mode, cleaned_input = self._detect_and_set_mode(user_input)
|
|
530
|
+
|
|
531
|
+
# Handle different modes
|
|
532
|
+
if mode == InputMode.BASH:
|
|
533
|
+
await self._handle_bash_mode(cleaned_input)
|
|
534
|
+
return
|
|
535
|
+
elif mode == InputMode.KODING:
|
|
536
|
+
await self._handle_koding_mode(cleaned_input)
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
# Handle prompt mode (regular AI chat)
|
|
540
|
+
try:
|
|
541
|
+
with Progress(
|
|
542
|
+
SpinnerColumn(),
|
|
543
|
+
TextColumn(
|
|
544
|
+
"[progress.description]{task.description} (Ctrl+C to interrupt)"
|
|
545
|
+
),
|
|
546
|
+
console=self.console,
|
|
547
|
+
) as progress:
|
|
548
|
+
task = progress.add_task("🤖 Processing...", total=None)
|
|
549
|
+
|
|
550
|
+
response = await self.process_input_with_interrupt(cleaned_input)
|
|
551
|
+
|
|
552
|
+
progress.update(task, completed=True)
|
|
553
|
+
|
|
554
|
+
if response is None:
|
|
555
|
+
# Task was cancelled
|
|
556
|
+
cancelled_panel = Panel(
|
|
557
|
+
"⚠️ [bold yellow]Task was interrupted![/bold yellow]",
|
|
558
|
+
title="[bold yellow]Interrupted[/bold yellow]",
|
|
559
|
+
border_style="yellow",
|
|
560
|
+
)
|
|
561
|
+
self.console.print(cancelled_panel)
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
# Display agent response with rich formatting
|
|
565
|
+
if "```" in response.answer:
|
|
566
|
+
agent_content = Markdown(response.answer)
|
|
567
|
+
else:
|
|
568
|
+
agent_content = response.answer
|
|
569
|
+
|
|
570
|
+
response_panel = Panel(
|
|
571
|
+
agent_content,
|
|
572
|
+
title="🤖 [bold green]Agent Response[/bold green]",
|
|
573
|
+
border_style="green",
|
|
574
|
+
)
|
|
575
|
+
self.console.print(response_panel)
|
|
576
|
+
|
|
577
|
+
if self.verbose:
|
|
578
|
+
self.console.print(
|
|
579
|
+
f"[dim]Response length: {len(response.answer)} characters[/dim]"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
except KeyboardInterrupt:
|
|
583
|
+
# Handle Ctrl+C during processing
|
|
584
|
+
self.interrupt_current_task()
|
|
585
|
+
cancelled_panel = Panel(
|
|
586
|
+
"⚠️ [bold yellow]Task interrupted by user![/bold yellow]",
|
|
587
|
+
title="[bold yellow]Interrupted[/bold yellow]",
|
|
588
|
+
border_style="yellow",
|
|
589
|
+
)
|
|
590
|
+
self.console.print(cancelled_panel)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
error_panel = Panel(
|
|
593
|
+
f"❌ [bold red]Error: {e}[/bold red]",
|
|
594
|
+
title="[bold red]Error[/bold red]",
|
|
595
|
+
border_style="red",
|
|
596
|
+
)
|
|
597
|
+
self.console.print(error_panel)
|
|
598
|
+
|
|
599
|
+
if self.verbose:
|
|
600
|
+
import traceback
|
|
601
|
+
|
|
602
|
+
self.console.print(
|
|
603
|
+
f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
async def process_command(self, command_input: str):
|
|
607
|
+
"""Process a command input with support for different command types."""
|
|
608
|
+
from minion_code.commands import CommandType
|
|
609
|
+
|
|
610
|
+
# Remove the leading /
|
|
611
|
+
command_input = (
|
|
612
|
+
command_input[1:] if command_input.startswith("/") else command_input
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Split command and arguments
|
|
616
|
+
parts = command_input.split(" ", 1)
|
|
617
|
+
command_name = parts[0].lower()
|
|
618
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
619
|
+
|
|
620
|
+
if self.verbose:
|
|
621
|
+
self.console.print(
|
|
622
|
+
f"[dim]Executing command: {command_name} with args: {args}[/dim]"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Get command class
|
|
626
|
+
command_class = command_registry.get_command(command_name)
|
|
627
|
+
if not command_class:
|
|
628
|
+
error_panel = Panel(
|
|
629
|
+
f"❌ [bold red]Unknown command: /{command_name}[/bold red]\n"
|
|
630
|
+
f"💡 [italic]Use '/help' to see available commands[/italic]",
|
|
631
|
+
title="[bold red]Error[/bold red]",
|
|
632
|
+
border_style="red",
|
|
633
|
+
)
|
|
634
|
+
self.console.print(error_panel)
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
# Get command type and is_skill
|
|
638
|
+
command_type = getattr(command_class, "command_type", CommandType.LOCAL)
|
|
639
|
+
is_skill = getattr(command_class, "is_skill", False)
|
|
640
|
+
|
|
641
|
+
# Handle PROMPT type commands - expand and send to LLM
|
|
642
|
+
if command_type == CommandType.PROMPT:
|
|
643
|
+
try:
|
|
644
|
+
output_adapter = RichOutputAdapter(self.console)
|
|
645
|
+
command_instance = command_class(output_adapter, self.agent)
|
|
646
|
+
expanded_prompt = await command_instance.get_prompt(args)
|
|
647
|
+
|
|
648
|
+
# Process expanded prompt through AI
|
|
649
|
+
self.console.print(
|
|
650
|
+
f"[dim]Expanded prompt: {expanded_prompt[:100]}...[/dim]"
|
|
651
|
+
if self.verbose
|
|
652
|
+
else ""
|
|
653
|
+
)
|
|
654
|
+
await self.process_input(expanded_prompt)
|
|
655
|
+
|
|
656
|
+
except Exception as e:
|
|
657
|
+
error_panel = Panel(
|
|
658
|
+
f"❌ [bold red]Error expanding command /{command_name}: {e}[/bold red]",
|
|
659
|
+
title="[bold red]Command Error[/bold red]",
|
|
660
|
+
border_style="red",
|
|
661
|
+
)
|
|
662
|
+
self.console.print(error_panel)
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
# Handle LOCAL and LOCAL_JSX type commands - direct execution
|
|
666
|
+
try:
|
|
667
|
+
# Show status message based on is_skill
|
|
668
|
+
if is_skill:
|
|
669
|
+
status_text = f"⚙️ /{command_name} skill is executing..."
|
|
670
|
+
else:
|
|
671
|
+
status_text = f"⚙️ /{command_name} is executing..."
|
|
672
|
+
|
|
673
|
+
self.console.print(f"[dim]{status_text}[/dim]")
|
|
674
|
+
|
|
675
|
+
# Wrap console in RichOutputAdapter for commands
|
|
676
|
+
output_adapter = RichOutputAdapter(self.console)
|
|
677
|
+
command_instance = command_class(output_adapter, self.agent)
|
|
678
|
+
|
|
679
|
+
# Special handling for quit command
|
|
680
|
+
if command_name in ["quit", "exit", "q", "bye"]:
|
|
681
|
+
command_instance._tui_instance = self
|
|
682
|
+
|
|
683
|
+
await command_instance.execute(args)
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
error_panel = Panel(
|
|
687
|
+
f"❌ [bold red]Error executing command /{command_name}: {e}[/bold red]",
|
|
688
|
+
title="[bold red]Command Error[/bold red]",
|
|
689
|
+
border_style="red",
|
|
690
|
+
)
|
|
691
|
+
self.console.print(error_panel)
|
|
692
|
+
|
|
693
|
+
if self.verbose:
|
|
694
|
+
import traceback
|
|
695
|
+
|
|
696
|
+
self.console.print(
|
|
697
|
+
f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]"
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def show_tools(self):
|
|
701
|
+
"""Show available tools in a beautiful table."""
|
|
702
|
+
if not self.agent or not self.agent.tools:
|
|
703
|
+
self.console.print("❌ No tools available")
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
tools_table = Table(
|
|
707
|
+
title="🛠️ Available Tools", show_header=True, header_style="bold magenta"
|
|
708
|
+
)
|
|
709
|
+
tools_table.add_column("Tool Name", style="cyan", no_wrap=True)
|
|
710
|
+
tools_table.add_column("Description", style="white")
|
|
711
|
+
tools_table.add_column("Source", style="yellow")
|
|
712
|
+
tools_table.add_column("Type", style="green")
|
|
713
|
+
|
|
714
|
+
# Separate MCP tools from built-in tools
|
|
715
|
+
mcp_tool_names = (
|
|
716
|
+
{tool.name for tool in self.mcp_tools} if self.mcp_tools else set()
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
for tool in self.agent.tools:
|
|
720
|
+
tool_type = (
|
|
721
|
+
"Read-only" if getattr(tool, "readonly", False) else "Read-write"
|
|
722
|
+
)
|
|
723
|
+
source = "MCP" if tool.name in mcp_tool_names else "Built-in"
|
|
724
|
+
|
|
725
|
+
tools_table.add_row(
|
|
726
|
+
tool.name,
|
|
727
|
+
(
|
|
728
|
+
tool.description[:60] + "..."
|
|
729
|
+
if len(tool.description) > 60
|
|
730
|
+
else tool.description
|
|
731
|
+
),
|
|
732
|
+
source,
|
|
733
|
+
tool_type,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
self.console.print(tools_table)
|
|
737
|
+
|
|
738
|
+
# Show summary
|
|
739
|
+
total_tools = len(self.agent.tools)
|
|
740
|
+
mcp_count = len(self.mcp_tools) if self.mcp_tools else 0
|
|
741
|
+
builtin_count = total_tools - mcp_count
|
|
742
|
+
|
|
743
|
+
summary_text = f"[dim]Total: {total_tools} tools"
|
|
744
|
+
if mcp_count > 0:
|
|
745
|
+
summary_text += f" (Built-in: {builtin_count}, MCP: {mcp_count})"
|
|
746
|
+
summary_text += "[/dim]"
|
|
747
|
+
|
|
748
|
+
self.console.print(summary_text)
|
|
749
|
+
|
|
750
|
+
def show_history(self):
|
|
751
|
+
"""Show conversation history in a beautiful format."""
|
|
752
|
+
if not self.agent:
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
history = self.agent.get_conversation_history()
|
|
756
|
+
if not history:
|
|
757
|
+
no_history_panel = Panel(
|
|
758
|
+
"📝 [italic]No conversation history yet.[/italic]",
|
|
759
|
+
title="[bold blue]History[/bold blue]",
|
|
760
|
+
border_style="blue",
|
|
761
|
+
)
|
|
762
|
+
self.console.print(no_history_panel)
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
history_panel = Panel(
|
|
766
|
+
f"📝 [bold blue]Conversation History ({len(history)} messages)[/bold blue]",
|
|
767
|
+
border_style="blue",
|
|
768
|
+
)
|
|
769
|
+
self.console.print(history_panel)
|
|
770
|
+
|
|
771
|
+
display_count = 5 if not self.verbose else 10
|
|
772
|
+
for i, entry in enumerate(history[-display_count:], 1):
|
|
773
|
+
# User message
|
|
774
|
+
user_panel = Panel(
|
|
775
|
+
(
|
|
776
|
+
entry["user_message"][:200] + "..."
|
|
777
|
+
if len(entry["user_message"]) > 200
|
|
778
|
+
else entry["user_message"]
|
|
779
|
+
),
|
|
780
|
+
title=f"👤 [bold cyan]You (Message {len(history) - display_count + i})[/bold cyan]",
|
|
781
|
+
border_style="cyan",
|
|
782
|
+
)
|
|
783
|
+
self.console.print(user_panel)
|
|
784
|
+
|
|
785
|
+
# Agent response
|
|
786
|
+
agent_response = (
|
|
787
|
+
entry["agent_response"][:200] + "..."
|
|
788
|
+
if len(entry["agent_response"]) > 200
|
|
789
|
+
else entry["agent_response"]
|
|
790
|
+
)
|
|
791
|
+
agent_panel = Panel(
|
|
792
|
+
agent_response,
|
|
793
|
+
title="🤖 [bold green]Agent[/bold green]",
|
|
794
|
+
border_style="green",
|
|
795
|
+
)
|
|
796
|
+
self.console.print(agent_panel)
|
|
797
|
+
self.console.print() # Add spacing
|
|
798
|
+
|
|
799
|
+
async def run(self):
|
|
800
|
+
"""Run the CLI."""
|
|
801
|
+
# Welcome banner
|
|
802
|
+
welcome_panel = Panel(
|
|
803
|
+
"🚀 [bold blue]MinionCodeAgent CLI[/bold blue]\n"
|
|
804
|
+
"💡 [italic]Use '/help' for commands or just chat with the agent![/italic]\n"
|
|
805
|
+
"⚠️ [italic]Press Ctrl+C during processing to interrupt tasks[/italic]\n"
|
|
806
|
+
"🛑 [italic]Type '/quit' to exit[/italic]",
|
|
807
|
+
title="[bold magenta]Welcome[/bold magenta]",
|
|
808
|
+
border_style="magenta",
|
|
809
|
+
)
|
|
810
|
+
self.console.print(welcome_panel)
|
|
811
|
+
|
|
812
|
+
await self.setup()
|
|
813
|
+
|
|
814
|
+
while self.running:
|
|
815
|
+
try:
|
|
816
|
+
# Show current mode in prompt
|
|
817
|
+
mode_indicator = self._get_mode_indicator(self.current_mode)
|
|
818
|
+
prompt_text = f"\n{mode_indicator} [bold cyan]You[/bold cyan]"
|
|
819
|
+
|
|
820
|
+
# Use rich prompt for better input experience
|
|
821
|
+
user_input = Prompt.ask(prompt_text, console=self.console).strip()
|
|
822
|
+
|
|
823
|
+
if user_input:
|
|
824
|
+
await self.process_input(user_input)
|
|
825
|
+
|
|
826
|
+
except (EOFError, KeyboardInterrupt):
|
|
827
|
+
# Handle Ctrl+C at input prompt
|
|
828
|
+
if self.current_task and not self.current_task.done():
|
|
829
|
+
# If there's a running task, interrupt it
|
|
830
|
+
self.interrupt_current_task()
|
|
831
|
+
else:
|
|
832
|
+
# If no running task, exit
|
|
833
|
+
goodbye_panel = Panel(
|
|
834
|
+
"\n👋 [bold yellow]Goodbye![/bold yellow]",
|
|
835
|
+
title="[bold red]Exit[/bold red]",
|
|
836
|
+
border_style="red",
|
|
837
|
+
)
|
|
838
|
+
self.console.print(goodbye_panel)
|
|
839
|
+
break
|
|
840
|
+
|
|
841
|
+
# Cleanup resources
|
|
842
|
+
await self.cleanup()
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
@app.command()
|
|
846
|
+
def main(
|
|
847
|
+
dir: Optional[str] = typer.Option(
|
|
848
|
+
None, "--dir", "-d", help="🗂️ Change to specified directory before starting"
|
|
849
|
+
),
|
|
850
|
+
verbose: bool = typer.Option(
|
|
851
|
+
False,
|
|
852
|
+
"--verbose",
|
|
853
|
+
"-v",
|
|
854
|
+
help="🔍 Enable verbose output with additional debugging information",
|
|
855
|
+
),
|
|
856
|
+
config: Optional[str] = typer.Option(
|
|
857
|
+
None, "--config", "-c", help="🔌 Path to MCP configuration file (JSON format)"
|
|
858
|
+
),
|
|
859
|
+
):
|
|
860
|
+
"""
|
|
861
|
+
🤖 Start the MinionCodeAgent CLI interface
|
|
862
|
+
|
|
863
|
+
An AI-powered code assistant with task interruption support and MCP tools integration.
|
|
864
|
+
"""
|
|
865
|
+
console = Console()
|
|
866
|
+
|
|
867
|
+
# Change directory if specified
|
|
868
|
+
if dir:
|
|
869
|
+
try:
|
|
870
|
+
target_dir = Path(dir).resolve()
|
|
871
|
+
if not target_dir.exists():
|
|
872
|
+
console.print(
|
|
873
|
+
f"❌ [bold red]Directory does not exist: {dir}[/bold red]"
|
|
874
|
+
)
|
|
875
|
+
raise typer.Exit(1)
|
|
876
|
+
if not target_dir.is_dir():
|
|
877
|
+
console.print(f"❌ [bold red]Path is not a directory: {dir}[/bold red]")
|
|
878
|
+
raise typer.Exit(1)
|
|
879
|
+
|
|
880
|
+
os.chdir(target_dir)
|
|
881
|
+
if verbose:
|
|
882
|
+
console.print(
|
|
883
|
+
f"📁 [bold green]Changed to directory: {target_dir}[/bold green]"
|
|
884
|
+
)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
console.print(f"❌ [bold red]Failed to change directory: {e}[/bold red]")
|
|
887
|
+
raise typer.Exit(1)
|
|
888
|
+
|
|
889
|
+
# Validate MCP config if provided
|
|
890
|
+
mcp_config_path = None
|
|
891
|
+
if config:
|
|
892
|
+
mcp_config_path = Path(config).resolve()
|
|
893
|
+
if not mcp_config_path.exists():
|
|
894
|
+
console.print(
|
|
895
|
+
f"❌ [bold red]MCP config file does not exist: {config}[/bold red]"
|
|
896
|
+
)
|
|
897
|
+
raise typer.Exit(1)
|
|
898
|
+
if not mcp_config_path.is_file():
|
|
899
|
+
console.print(
|
|
900
|
+
f"❌ [bold red]MCP config path is not a file: {config}[/bold red]"
|
|
901
|
+
)
|
|
902
|
+
raise typer.Exit(1)
|
|
903
|
+
|
|
904
|
+
if verbose:
|
|
905
|
+
console.print(
|
|
906
|
+
f"🔌 [bold green]Using MCP config: {mcp_config_path}[/bold green]"
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Create and run CLI
|
|
910
|
+
cli = InterruptibleCLI(verbose=verbose, mcp_config=mcp_config_path)
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
asyncio.run(cli.run())
|
|
914
|
+
except KeyboardInterrupt:
|
|
915
|
+
console.print("\n👋 [bold yellow]Goodbye![/bold yellow]")
|
|
916
|
+
|
|
917
|
+
|
|
2
918
|
def run():
|
|
3
919
|
app()
|
|
4
|
-
|
|
5
|
-
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
if __name__ == "__main__":
|
|
923
|
+
app()
|