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.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.1.dist-info/METADATA +475 -0
  98. minion_code-0.1.1.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.1.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.1.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()