minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.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.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Simple CLI interface for MinionCodeAgent using Typer (Console-based)
|
|
5
|
+
|
|
6
|
+
This is the original console-based CLI interface, preserved for compatibility.
|
|
7
|
+
For the modern TUI interface, use cli.py or the 'repl' command.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich.markdown import Markdown
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
23
|
+
from rich.prompt import Prompt
|
|
24
|
+
|
|
25
|
+
# Add project root to path
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
27
|
+
|
|
28
|
+
from minion_code import MinionCodeAgent
|
|
29
|
+
from minion_code.commands import command_registry
|
|
30
|
+
from minion_code.utils.mcp_loader import MCPToolsLoader
|
|
31
|
+
from minion_code.adapters import RichOutputAdapter
|
|
32
|
+
from minion_code.agents.hooks import create_cli_hooks, SpinnerController
|
|
33
|
+
from minion_code.utils.session_storage import (
|
|
34
|
+
Session,
|
|
35
|
+
create_session,
|
|
36
|
+
save_session,
|
|
37
|
+
load_session,
|
|
38
|
+
get_latest_session_id,
|
|
39
|
+
add_message,
|
|
40
|
+
restore_agent_history,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
app = typer.Typer(
|
|
44
|
+
name="minion-code-simple",
|
|
45
|
+
help="🤖 MinionCodeAgent Simple CLI - Console-based interface",
|
|
46
|
+
add_completion=False,
|
|
47
|
+
rich_markup_mode="rich",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InterruptibleCLI:
|
|
52
|
+
"""CLI with task interruption support using standard input."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
verbose: bool = False,
|
|
57
|
+
mcp_config: Optional[Path] = None,
|
|
58
|
+
resume_session_id: Optional[str] = None,
|
|
59
|
+
continue_last: bool = False,
|
|
60
|
+
initial_prompt: Optional[str] = None,
|
|
61
|
+
print_output: bool = False,
|
|
62
|
+
auto_accept: bool = False,
|
|
63
|
+
model: Optional[str] = None,
|
|
64
|
+
):
|
|
65
|
+
self.agent = None
|
|
66
|
+
self.running = True
|
|
67
|
+
self.console = Console()
|
|
68
|
+
self.verbose = verbose
|
|
69
|
+
self.mcp_config = mcp_config
|
|
70
|
+
self.mcp_tools = []
|
|
71
|
+
self.mcp_loader = None
|
|
72
|
+
self.current_task = None
|
|
73
|
+
self.task_cancelled = False
|
|
74
|
+
self.interrupt_requested = False
|
|
75
|
+
|
|
76
|
+
# Initial prompt support (like claude "prompt")
|
|
77
|
+
self.initial_prompt = initial_prompt
|
|
78
|
+
self.print_output = print_output # Print output and exit (non-interactive)
|
|
79
|
+
|
|
80
|
+
# Tool permission mode
|
|
81
|
+
self.auto_accept = auto_accept
|
|
82
|
+
|
|
83
|
+
# LLM model override
|
|
84
|
+
self.model = model
|
|
85
|
+
|
|
86
|
+
# Session management
|
|
87
|
+
self.session: Optional[Session] = None
|
|
88
|
+
self.resume_session_id = resume_session_id
|
|
89
|
+
self.continue_last = continue_last
|
|
90
|
+
|
|
91
|
+
# Create output adapter for commands
|
|
92
|
+
self.output_adapter = RichOutputAdapter(self.console)
|
|
93
|
+
|
|
94
|
+
# Spinner controller for pausing during permission prompts
|
|
95
|
+
self.spinner_controller = SpinnerController()
|
|
96
|
+
|
|
97
|
+
async def setup(self):
|
|
98
|
+
"""Setup the agent."""
|
|
99
|
+
with Progress(
|
|
100
|
+
SpinnerColumn(),
|
|
101
|
+
TextColumn("[progress.description]{task.description}"),
|
|
102
|
+
console=self.console,
|
|
103
|
+
) as progress:
|
|
104
|
+
# Load MCP tools - auto-discover if not explicitly provided
|
|
105
|
+
mcp_task = progress.add_task("🔌 Loading MCP tools...", total=None)
|
|
106
|
+
try:
|
|
107
|
+
# MCPToolsLoader will auto-discover config if mcp_config is None
|
|
108
|
+
self.mcp_loader = MCPToolsLoader(self.mcp_config, auto_discover=True)
|
|
109
|
+
|
|
110
|
+
if self.mcp_loader.config_path:
|
|
111
|
+
if self.verbose:
|
|
112
|
+
self.console.print(
|
|
113
|
+
f"[dim]Using MCP config: {self.mcp_loader.config_path}[/dim]"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.mcp_loader.load_config()
|
|
117
|
+
self.mcp_tools = await self.mcp_loader.load_all_tools()
|
|
118
|
+
|
|
119
|
+
if self.mcp_tools:
|
|
120
|
+
self.console.print(
|
|
121
|
+
f"✅ Loaded {len(self.mcp_tools)} MCP tools from {self.mcp_loader.config_path}"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
server_info = self.mcp_loader.get_server_info()
|
|
125
|
+
if server_info:
|
|
126
|
+
self.console.print(
|
|
127
|
+
f"📋 Found {len(server_info)} MCP server(s) configured"
|
|
128
|
+
)
|
|
129
|
+
for name, info in server_info.items():
|
|
130
|
+
status = "disabled" if info["disabled"] else "enabled"
|
|
131
|
+
self.console.print(
|
|
132
|
+
f" - {name}: {info['command']} ({status})"
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
self.console.print("⚠️ No MCP servers found in config")
|
|
136
|
+
else:
|
|
137
|
+
if self.verbose:
|
|
138
|
+
self.console.print(
|
|
139
|
+
"[dim]No MCP config found in standard locations[/dim]"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
progress.update(mcp_task, completed=True)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.console.print(f"❌ Failed to load MCP tools: {e}")
|
|
145
|
+
if self.verbose:
|
|
146
|
+
import traceback
|
|
147
|
+
|
|
148
|
+
self.console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
149
|
+
progress.update(mcp_task, completed=True)
|
|
150
|
+
|
|
151
|
+
agent_task = progress.add_task(
|
|
152
|
+
"🔧 Setting up MinionCodeAgent...", total=None
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Create hooks for tool permission control
|
|
156
|
+
hooks = create_cli_hooks(
|
|
157
|
+
auto_accept=self.auto_accept,
|
|
158
|
+
spinner_controller=self.spinner_controller,
|
|
159
|
+
console=self.console,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Use model from CLI if provided, otherwise use default
|
|
163
|
+
llm_model = self.model if self.model else "claude-sonnet-4-5"
|
|
164
|
+
if self.model and self.verbose:
|
|
165
|
+
self.console.print(f"[dim]Using model: {self.model}[/dim]")
|
|
166
|
+
|
|
167
|
+
self.agent = await MinionCodeAgent.create(
|
|
168
|
+
name="CLI Code Assistant",
|
|
169
|
+
llm=llm_model,
|
|
170
|
+
additional_tools=self.mcp_tools if self.mcp_tools else None,
|
|
171
|
+
hooks=hooks,
|
|
172
|
+
# History decay: save large outputs to file after N steps
|
|
173
|
+
decay_enabled=True,
|
|
174
|
+
decay_ttl_steps=3,
|
|
175
|
+
decay_min_size=100_000, # 100KB
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
progress.update(agent_task, completed=True)
|
|
179
|
+
|
|
180
|
+
# Show setup summary
|
|
181
|
+
total_tools = len(self.agent.tools)
|
|
182
|
+
mcp_count = len(self.mcp_tools)
|
|
183
|
+
builtin_count = total_tools - mcp_count
|
|
184
|
+
|
|
185
|
+
summary_text = (
|
|
186
|
+
f"✅ Agent ready with [bold green]{total_tools}[/bold green] tools!"
|
|
187
|
+
)
|
|
188
|
+
if mcp_count > 0:
|
|
189
|
+
summary_text += f"\n🔌 MCP tools: [bold cyan]{mcp_count}[/bold cyan]"
|
|
190
|
+
summary_text += (
|
|
191
|
+
f"\n🛠️ Built-in tools: [bold blue]{builtin_count}[/bold blue]"
|
|
192
|
+
)
|
|
193
|
+
summary_text += f"\n⚠️ [bold yellow]Press Ctrl+C during processing to interrupt tasks[/bold yellow]"
|
|
194
|
+
|
|
195
|
+
success_panel = Panel(
|
|
196
|
+
summary_text,
|
|
197
|
+
title="[bold green]Setup Complete[/bold green]",
|
|
198
|
+
border_style="green",
|
|
199
|
+
)
|
|
200
|
+
self.console.print(success_panel)
|
|
201
|
+
|
|
202
|
+
if self.verbose:
|
|
203
|
+
self.console.print(f"[dim]Working directory: {os.getcwd()}[/dim]")
|
|
204
|
+
|
|
205
|
+
# Handle session restoration
|
|
206
|
+
await self._init_session()
|
|
207
|
+
|
|
208
|
+
async def _init_session(self):
|
|
209
|
+
"""Initialize or restore session."""
|
|
210
|
+
current_project = os.getcwd()
|
|
211
|
+
|
|
212
|
+
# Try to restore session if requested
|
|
213
|
+
if self.resume_session_id:
|
|
214
|
+
self.session = load_session(self.resume_session_id)
|
|
215
|
+
if self.session:
|
|
216
|
+
self.console.print(
|
|
217
|
+
Panel(
|
|
218
|
+
f"Restored session `{self.resume_session_id}` "
|
|
219
|
+
f"({len(self.session.messages)} messages)\n"
|
|
220
|
+
f"Title: {self.session.metadata.title or '(none)'}",
|
|
221
|
+
title="[bold green]Session Restored[/bold green]",
|
|
222
|
+
border_style="green",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
# Restore agent history
|
|
226
|
+
restore_agent_history(self.agent, self.session, self.verbose)
|
|
227
|
+
else:
|
|
228
|
+
self.console.print(
|
|
229
|
+
Panel(
|
|
230
|
+
f"Session `{self.resume_session_id}` not found. Starting new session.",
|
|
231
|
+
title="[bold yellow]Warning[/bold yellow]",
|
|
232
|
+
border_style="yellow",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
self.session = create_session(current_project)
|
|
236
|
+
elif self.continue_last:
|
|
237
|
+
latest_id = get_latest_session_id(project_path=current_project)
|
|
238
|
+
if latest_id:
|
|
239
|
+
self.session = load_session(latest_id)
|
|
240
|
+
if self.session:
|
|
241
|
+
self.console.print(
|
|
242
|
+
Panel(
|
|
243
|
+
f"Continuing session `{latest_id}` "
|
|
244
|
+
f"({len(self.session.messages)} messages)\n"
|
|
245
|
+
f"Title: {self.session.metadata.title or '(none)'}",
|
|
246
|
+
title="[bold green]Session Continued[/bold green]",
|
|
247
|
+
border_style="green",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
# Restore agent history
|
|
251
|
+
restore_agent_history(self.agent, self.session, self.verbose)
|
|
252
|
+
else:
|
|
253
|
+
self.session = create_session(current_project)
|
|
254
|
+
else:
|
|
255
|
+
self.console.print(
|
|
256
|
+
Panel(
|
|
257
|
+
"No previous session found. Starting new session.",
|
|
258
|
+
title="[bold yellow]Note[/bold yellow]",
|
|
259
|
+
border_style="yellow",
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
self.session = create_session(current_project)
|
|
263
|
+
else:
|
|
264
|
+
# Create new session
|
|
265
|
+
self.session = create_session(current_project)
|
|
266
|
+
|
|
267
|
+
if self.verbose and self.session:
|
|
268
|
+
self.console.print(
|
|
269
|
+
f"[dim]Session ID: {self.session.metadata.session_id}[/dim]"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
async def process_input_with_interrupt(self, user_input: str):
|
|
273
|
+
"""Process user input with interrupt support."""
|
|
274
|
+
self.task_cancelled = False
|
|
275
|
+
self.interrupt_requested = False
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
# Create the actual processing task
|
|
279
|
+
async def processing_task():
|
|
280
|
+
response = await self.agent.run_async(user_input)
|
|
281
|
+
return response
|
|
282
|
+
|
|
283
|
+
# Start the task
|
|
284
|
+
self.current_task = asyncio.create_task(processing_task())
|
|
285
|
+
|
|
286
|
+
# Monitor for cancellation while task runs
|
|
287
|
+
while not self.current_task.done():
|
|
288
|
+
if self.interrupt_requested:
|
|
289
|
+
self.current_task.cancel()
|
|
290
|
+
try:
|
|
291
|
+
await self.current_task
|
|
292
|
+
except asyncio.CancelledError:
|
|
293
|
+
pass
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
await asyncio.sleep(0.1) # Check every 100ms
|
|
297
|
+
|
|
298
|
+
# Get the result
|
|
299
|
+
response = await self.current_task
|
|
300
|
+
return response
|
|
301
|
+
|
|
302
|
+
except asyncio.CancelledError:
|
|
303
|
+
return None
|
|
304
|
+
finally:
|
|
305
|
+
self.current_task = None
|
|
306
|
+
|
|
307
|
+
def interrupt_current_task(self):
|
|
308
|
+
"""Interrupt the current running task."""
|
|
309
|
+
if self.current_task and not self.current_task.done():
|
|
310
|
+
self.interrupt_requested = True
|
|
311
|
+
self.console.print(
|
|
312
|
+
"\n⚠️ [bold yellow]Task interruption requested...[/bold yellow]"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def cleanup(self):
|
|
316
|
+
"""Clean up resources."""
|
|
317
|
+
if self.mcp_loader:
|
|
318
|
+
try:
|
|
319
|
+
await self.mcp_loader.close()
|
|
320
|
+
except Exception as e:
|
|
321
|
+
if self.verbose:
|
|
322
|
+
self.console.print(f"[dim]Error during MCP cleanup: {e}[/dim]")
|
|
323
|
+
|
|
324
|
+
async def process_input(self, user_input: str):
|
|
325
|
+
"""Process user input."""
|
|
326
|
+
user_input = user_input.strip()
|
|
327
|
+
|
|
328
|
+
if self.verbose:
|
|
329
|
+
self.console.print(
|
|
330
|
+
f"[dim]Processing input: {user_input[:50]}{'...' if len(user_input) > 50 else ''}[/dim]"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Check if it's a command (starts with /)
|
|
334
|
+
if user_input.startswith("/"):
|
|
335
|
+
await self.process_command(user_input)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Process with agent
|
|
339
|
+
try:
|
|
340
|
+
with Progress(
|
|
341
|
+
SpinnerColumn(),
|
|
342
|
+
TextColumn("[progress.description]{task.description}"),
|
|
343
|
+
console=self.console,
|
|
344
|
+
) as progress:
|
|
345
|
+
task = progress.add_task("🤖 Processing...", total=None)
|
|
346
|
+
|
|
347
|
+
# Set progress on spinner controller so hooks can pause/resume it
|
|
348
|
+
self.spinner_controller.set_progress(progress)
|
|
349
|
+
try:
|
|
350
|
+
response = await self.process_input_with_interrupt(user_input)
|
|
351
|
+
finally:
|
|
352
|
+
self.spinner_controller.clear_progress()
|
|
353
|
+
|
|
354
|
+
progress.update(task, completed=True)
|
|
355
|
+
|
|
356
|
+
if response is None:
|
|
357
|
+
# Task was cancelled
|
|
358
|
+
cancelled_panel = Panel(
|
|
359
|
+
"⚠️ [bold yellow]Task was interrupted![/bold yellow]",
|
|
360
|
+
title="[bold yellow]Interrupted[/bold yellow]",
|
|
361
|
+
border_style="yellow",
|
|
362
|
+
)
|
|
363
|
+
self.console.print(cancelled_panel)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Save to session
|
|
367
|
+
if self.session:
|
|
368
|
+
add_message(self.session, "user", user_input)
|
|
369
|
+
add_message(self.session, "assistant", response.answer)
|
|
370
|
+
|
|
371
|
+
# Display agent response with rich formatting
|
|
372
|
+
if "```" in response.answer:
|
|
373
|
+
agent_content = Markdown(response.answer)
|
|
374
|
+
else:
|
|
375
|
+
agent_content = response.answer
|
|
376
|
+
|
|
377
|
+
response_panel = Panel(
|
|
378
|
+
agent_content,
|
|
379
|
+
title="🤖 [bold green]Agent Response[/bold green]",
|
|
380
|
+
border_style="green",
|
|
381
|
+
)
|
|
382
|
+
self.console.print(response_panel)
|
|
383
|
+
|
|
384
|
+
if self.verbose:
|
|
385
|
+
self.console.print(
|
|
386
|
+
f"[dim]Response length: {len(response.answer)} characters[/dim]"
|
|
387
|
+
)
|
|
388
|
+
if self.session:
|
|
389
|
+
self.console.print(
|
|
390
|
+
f"[dim]Session: {self.session.metadata.session_id} ({len(self.session.messages)} msgs)[/dim]"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
except KeyboardInterrupt:
|
|
394
|
+
# Handle Ctrl+C during processing
|
|
395
|
+
self.interrupt_current_task()
|
|
396
|
+
cancelled_panel = Panel(
|
|
397
|
+
"⚠️ [bold yellow]Task interrupted by user![/bold yellow]",
|
|
398
|
+
title="[bold yellow]Interrupted[/bold yellow]",
|
|
399
|
+
border_style="yellow",
|
|
400
|
+
)
|
|
401
|
+
self.console.print(cancelled_panel)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
error_panel = Panel(
|
|
404
|
+
f"❌ [bold red]Error: {e}[/bold red]",
|
|
405
|
+
title="[bold red]Error[/bold red]",
|
|
406
|
+
border_style="red",
|
|
407
|
+
)
|
|
408
|
+
self.console.print(error_panel)
|
|
409
|
+
|
|
410
|
+
if self.verbose:
|
|
411
|
+
import traceback
|
|
412
|
+
|
|
413
|
+
self.console.print(
|
|
414
|
+
f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
async def process_command(self, command_input: str):
|
|
418
|
+
"""Process a command input with support for different command types."""
|
|
419
|
+
from minion_code.commands import CommandType
|
|
420
|
+
|
|
421
|
+
# Remove the leading /
|
|
422
|
+
command_input = (
|
|
423
|
+
command_input[1:] if command_input.startswith("/") else command_input
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Split command and arguments
|
|
427
|
+
parts = command_input.split(" ", 1)
|
|
428
|
+
command_name = parts[0].lower()
|
|
429
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
430
|
+
|
|
431
|
+
if self.verbose:
|
|
432
|
+
self.console.print(
|
|
433
|
+
f"[dim]Executing command: {command_name} with args: {args}[/dim]"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Get command class
|
|
437
|
+
command_class = command_registry.get_command(command_name)
|
|
438
|
+
if not command_class:
|
|
439
|
+
error_panel = Panel(
|
|
440
|
+
f"❌ [bold red]Unknown command: /{command_name}[/bold red]\n"
|
|
441
|
+
f"💡 [italic]Use '/help' to see available commands[/italic]",
|
|
442
|
+
title="[bold red]Error[/bold red]",
|
|
443
|
+
border_style="red",
|
|
444
|
+
)
|
|
445
|
+
self.console.print(error_panel)
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
# Get command type and is_skill
|
|
449
|
+
command_type = getattr(command_class, "command_type", CommandType.LOCAL)
|
|
450
|
+
is_skill = getattr(command_class, "is_skill", False)
|
|
451
|
+
|
|
452
|
+
# Handle PROMPT type commands - expand and send to LLM
|
|
453
|
+
if command_type == CommandType.PROMPT:
|
|
454
|
+
try:
|
|
455
|
+
command_instance = command_class(self.output_adapter, self.agent)
|
|
456
|
+
expanded_prompt = await command_instance.get_prompt(args)
|
|
457
|
+
|
|
458
|
+
# Process expanded prompt through AI
|
|
459
|
+
if self.verbose:
|
|
460
|
+
self.console.print(
|
|
461
|
+
f"[dim]Expanded prompt: {expanded_prompt[:100]}...[/dim]"
|
|
462
|
+
)
|
|
463
|
+
await self.process_input(expanded_prompt)
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
error_panel = Panel(
|
|
467
|
+
f"❌ [bold red]Error expanding command /{command_name}: {e}[/bold red]",
|
|
468
|
+
title="[bold red]Command Error[/bold red]",
|
|
469
|
+
border_style="red",
|
|
470
|
+
)
|
|
471
|
+
self.console.print(error_panel)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# Handle LOCAL and LOCAL_JSX type commands - direct execution
|
|
475
|
+
try:
|
|
476
|
+
# Show status message based on is_skill
|
|
477
|
+
if is_skill:
|
|
478
|
+
status_text = f"⚙️ /{command_name} skill is executing..."
|
|
479
|
+
else:
|
|
480
|
+
status_text = f"⚙️ /{command_name} is executing..."
|
|
481
|
+
|
|
482
|
+
self.console.print(f"[dim]{status_text}[/dim]")
|
|
483
|
+
|
|
484
|
+
command_instance = command_class(self.output_adapter, self.agent)
|
|
485
|
+
|
|
486
|
+
# Special handling for quit command
|
|
487
|
+
if command_name in ["quit", "exit", "q", "bye"]:
|
|
488
|
+
command_instance._tui_instance = self
|
|
489
|
+
|
|
490
|
+
await command_instance.execute(args)
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
error_panel = Panel(
|
|
494
|
+
f"❌ [bold red]Error executing command /{command_name}: {e}[/bold red]",
|
|
495
|
+
title="[bold red]Command Error[/bold red]",
|
|
496
|
+
border_style="red",
|
|
497
|
+
)
|
|
498
|
+
self.console.print(error_panel)
|
|
499
|
+
|
|
500
|
+
if self.verbose:
|
|
501
|
+
import traceback
|
|
502
|
+
|
|
503
|
+
self.console.print(
|
|
504
|
+
f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
async def run(self):
|
|
508
|
+
"""Run the CLI."""
|
|
509
|
+
# Welcome banner (skip in print mode for cleaner output)
|
|
510
|
+
if not self.print_output:
|
|
511
|
+
welcome_panel = Panel(
|
|
512
|
+
"🚀 [bold blue]MinionCodeAgent Simple CLI[/bold blue]\n"
|
|
513
|
+
"💡 [italic]Use '/help' for commands or just chat with the agent![/italic]\n"
|
|
514
|
+
"⚠️ [italic]Press Ctrl+C during processing to interrupt tasks[/italic]\n"
|
|
515
|
+
"🛑 [italic]Type '/quit' to exit[/italic]",
|
|
516
|
+
title="[bold magenta]Welcome[/bold magenta]",
|
|
517
|
+
border_style="magenta",
|
|
518
|
+
)
|
|
519
|
+
self.console.print(welcome_panel)
|
|
520
|
+
|
|
521
|
+
await self.setup()
|
|
522
|
+
|
|
523
|
+
# Process initial prompt if provided (like claude "prompt")
|
|
524
|
+
if self.initial_prompt:
|
|
525
|
+
await self.process_input(self.initial_prompt)
|
|
526
|
+
|
|
527
|
+
# If print mode, exit after getting the response
|
|
528
|
+
if self.print_output:
|
|
529
|
+
await self.cleanup()
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
while self.running:
|
|
533
|
+
try:
|
|
534
|
+
# Use rich prompt for better input experience
|
|
535
|
+
user_input = Prompt.ask(
|
|
536
|
+
"\n[bold cyan]👤 You[/bold cyan]", console=self.console
|
|
537
|
+
).strip()
|
|
538
|
+
|
|
539
|
+
if user_input:
|
|
540
|
+
await self.process_input(user_input)
|
|
541
|
+
|
|
542
|
+
except (EOFError, KeyboardInterrupt):
|
|
543
|
+
# Handle Ctrl+C at input prompt
|
|
544
|
+
if self.current_task and not self.current_task.done():
|
|
545
|
+
# If there's a running task, interrupt it
|
|
546
|
+
self.interrupt_current_task()
|
|
547
|
+
else:
|
|
548
|
+
# If no running task, exit
|
|
549
|
+
goodbye_panel = Panel(
|
|
550
|
+
"\n👋 [bold yellow]Goodbye![/bold yellow]",
|
|
551
|
+
title="[bold red]Exit[/bold red]",
|
|
552
|
+
border_style="red",
|
|
553
|
+
)
|
|
554
|
+
self.console.print(goodbye_panel)
|
|
555
|
+
break
|
|
556
|
+
|
|
557
|
+
# Cleanup resources
|
|
558
|
+
await self.cleanup()
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@app.command()
|
|
562
|
+
def main(
|
|
563
|
+
prompt: Optional[str] = typer.Argument(
|
|
564
|
+
None, help="Initial prompt to send to the agent (like 'claude \"prompt\"')"
|
|
565
|
+
),
|
|
566
|
+
dir: Optional[str] = typer.Option(
|
|
567
|
+
None, "--dir", "-d", help="Change to specified directory before starting"
|
|
568
|
+
),
|
|
569
|
+
verbose: bool = typer.Option(
|
|
570
|
+
False,
|
|
571
|
+
"--verbose",
|
|
572
|
+
"-v",
|
|
573
|
+
help="Enable verbose output with additional debugging information",
|
|
574
|
+
),
|
|
575
|
+
config: Optional[str] = typer.Option(
|
|
576
|
+
None, "--config", "-c", help="Path to MCP configuration file (JSON format)"
|
|
577
|
+
),
|
|
578
|
+
continue_session: bool = typer.Option(
|
|
579
|
+
False, "--continue", help="Continue the most recent session for this project"
|
|
580
|
+
),
|
|
581
|
+
resume: Optional[str] = typer.Option(
|
|
582
|
+
None, "--resume", "-r", help="Resume a specific session by ID"
|
|
583
|
+
),
|
|
584
|
+
print_output: bool = typer.Option(
|
|
585
|
+
False, "--print", "-p", help="Print output and exit (non-interactive mode)"
|
|
586
|
+
),
|
|
587
|
+
dangerously_skip_permissions: bool = typer.Option(
|
|
588
|
+
False,
|
|
589
|
+
"--dangerously-skip-permissions",
|
|
590
|
+
help="Skip tool permission prompts (auto-accept all). Use with caution!",
|
|
591
|
+
),
|
|
592
|
+
):
|
|
593
|
+
"""
|
|
594
|
+
🤖 Start the MinionCodeAgent Simple CLI interface
|
|
595
|
+
|
|
596
|
+
Console-based AI-powered code assistant with task interruption support.
|
|
597
|
+
"""
|
|
598
|
+
console = Console()
|
|
599
|
+
|
|
600
|
+
# Change directory if specified
|
|
601
|
+
if dir:
|
|
602
|
+
try:
|
|
603
|
+
target_dir = Path(dir).resolve()
|
|
604
|
+
if not target_dir.exists():
|
|
605
|
+
console.print(
|
|
606
|
+
f"❌ [bold red]Directory does not exist: {dir}[/bold red]"
|
|
607
|
+
)
|
|
608
|
+
raise typer.Exit(1)
|
|
609
|
+
if not target_dir.is_dir():
|
|
610
|
+
console.print(f"❌ [bold red]Path is not a directory: {dir}[/bold red]")
|
|
611
|
+
raise typer.Exit(1)
|
|
612
|
+
|
|
613
|
+
os.chdir(target_dir)
|
|
614
|
+
if verbose:
|
|
615
|
+
console.print(
|
|
616
|
+
f"📁 [bold green]Changed to directory: {target_dir}[/bold green]"
|
|
617
|
+
)
|
|
618
|
+
except Exception as e:
|
|
619
|
+
console.print(f"❌ [bold red]Failed to change directory: {e}[/bold red]")
|
|
620
|
+
raise typer.Exit(1)
|
|
621
|
+
|
|
622
|
+
# Validate MCP config if provided
|
|
623
|
+
mcp_config_path = None
|
|
624
|
+
if config:
|
|
625
|
+
mcp_config_path = Path(config).resolve()
|
|
626
|
+
if not mcp_config_path.exists():
|
|
627
|
+
console.print(
|
|
628
|
+
f"❌ [bold red]MCP config file does not exist: {config}[/bold red]"
|
|
629
|
+
)
|
|
630
|
+
raise typer.Exit(1)
|
|
631
|
+
if not mcp_config_path.is_file():
|
|
632
|
+
console.print(
|
|
633
|
+
f"❌ [bold red]MCP config path is not a file: {config}[/bold red]"
|
|
634
|
+
)
|
|
635
|
+
raise typer.Exit(1)
|
|
636
|
+
|
|
637
|
+
if verbose:
|
|
638
|
+
console.print(
|
|
639
|
+
f"🔌 [bold green]Using MCP config: {mcp_config_path}[/bold green]"
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Create and run CLI
|
|
643
|
+
cli = InterruptibleCLI(
|
|
644
|
+
verbose=verbose,
|
|
645
|
+
mcp_config=mcp_config_path,
|
|
646
|
+
resume_session_id=resume,
|
|
647
|
+
continue_last=continue_session,
|
|
648
|
+
initial_prompt=prompt,
|
|
649
|
+
print_output=print_output,
|
|
650
|
+
auto_accept=dangerously_skip_permissions,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
asyncio.run(cli.run())
|
|
655
|
+
except KeyboardInterrupt:
|
|
656
|
+
console.print("\n👋 [bold yellow]Goodbye![/bold yellow]")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def run():
|
|
660
|
+
"""Entry point for pyproject.toml scripts."""
|
|
661
|
+
app()
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
if __name__ == "__main__":
|
|
665
|
+
app()
|