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.
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.2.dist-info/METADATA +476 -0
  98. minion_code-0.1.2.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.2.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.2.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
minion_code/cli.py CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- CLI interface for MinionCodeAgent using Typer
4
+ Modern CLI interface for MinionCodeAgent with Textual TUI support
5
5
 
6
- This CLI provides command-line arguments support including --dir and --verbose options.
6
+ This CLI provides both console and TUI interfaces:
7
+ - Default: Modern REPL TUI interface
8
+ - Console: Traditional console interface (--console flag)
7
9
  """
8
10
 
9
11
  import asyncio
@@ -14,489 +16,596 @@ from typing import Optional
14
16
 
15
17
  import typer
16
18
  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
19
 
24
20
  # Add project root to path
25
21
  sys.path.insert(0, str(Path(__file__).parent.parent))
26
22
 
27
- from minion_code import MinionCodeAgent
28
- from minion_code.commands import command_registry
29
- from minion_code.utils.mcp_loader import MCPToolsLoader
30
-
31
23
  app = typer.Typer(
32
24
  name="minion-code",
33
- help="🤖 MinionCodeAgent CLI - An AI-powered code assistant",
25
+ help="🤖 MinionCodeAgent CLI - Modern AI-powered code assistant",
34
26
  add_completion=False,
35
- rich_markup_mode="rich"
27
+ rich_markup_mode="rich",
36
28
  )
37
29
 
38
30
 
39
- class InterruptibleCLI:
40
- """CLI with task interruption support using standard input."""
41
-
42
- def __init__(self, verbose: bool = False, mcp_config: Optional[Path] = None):
43
- self.agent = None
44
- self.running = True
45
- self.console = Console()
46
- self.verbose = verbose
47
- self.mcp_config = mcp_config
48
- self.mcp_tools = []
49
- self.mcp_loader = None
50
- self.current_task = None
51
- self.task_cancelled = False
52
- self.interrupt_requested = False
53
-
54
- async def setup(self):
55
- """Setup the agent."""
56
- with Progress(
57
- SpinnerColumn(),
58
- TextColumn("[progress.description]{task.description}"),
59
- console=self.console,
60
- ) as progress:
61
- # Load MCP tools if config provided
62
- mcp_task = None
63
- if self.mcp_config:
64
- mcp_task = progress.add_task("🔌 Loading MCP tools...", total=None)
65
- try:
66
- self.mcp_loader = MCPToolsLoader(self.mcp_config)
67
- self.mcp_loader.load_config()
68
- self.mcp_tools = await self.mcp_loader.load_all_tools()
69
-
70
- if self.mcp_tools:
71
- self.console.print(f"✅ Loaded {len(self.mcp_tools)} MCP tools")
72
- else:
73
- server_info = self.mcp_loader.get_server_info()
74
- if server_info:
75
- self.console.print(f"📋 Found {len(server_info)} MCP server(s) configured")
76
- for name, info in server_info.items():
77
- status = "disabled" if info['disabled'] else "enabled"
78
- self.console.print(f" - {name}: {info['command']} ({status})")
79
- else:
80
- self.console.print("⚠️ No MCP servers found in config")
81
-
82
- progress.update(mcp_task, completed=True)
83
- except Exception as e:
84
- self.console.print(f"❌ Failed to load MCP tools: {e}")
85
- if self.verbose:
86
- import traceback
87
- self.console.print(f"[dim]{traceback.format_exc()}[/dim]")
88
-
89
- agent_task = progress.add_task("🔧 Setting up MinionCodeAgent...", total=None)
90
-
91
- self.agent = await MinionCodeAgent.create(
92
- name="CLI Code Assistant",
93
- llm="sonnet", # 使用更稳定的模型配置
94
- additional_tools=self.mcp_tools if self.mcp_tools else None
95
- )
96
-
97
- progress.update(agent_task, completed=True)
98
-
99
- # Show setup summary
100
- total_tools = len(self.agent.tools)
101
- mcp_count = len(self.mcp_tools)
102
- builtin_count = total_tools - mcp_count
103
-
104
- summary_text = f"✅ Agent ready with [bold green]{total_tools}[/bold green] tools!"
105
- if mcp_count > 0:
106
- summary_text += f"\n🔌 MCP tools: [bold cyan]{mcp_count}[/bold cyan]"
107
- summary_text += f"\n🛠️ Built-in tools: [bold blue]{builtin_count}[/bold blue]"
108
- summary_text += f"\n⚠️ [bold yellow]Press Ctrl+C during processing to interrupt tasks[/bold yellow]"
109
-
110
- success_panel = Panel(
111
- summary_text,
112
- title="[bold green]Setup Complete[/bold green]",
113
- border_style="green"
31
+ def run_console_cli(
32
+ verbose: bool = False,
33
+ mcp_config: Optional[Path] = None,
34
+ resume_session_id: Optional[str] = None,
35
+ continue_last: bool = False,
36
+ initial_prompt: Optional[str] = None,
37
+ print_output: bool = False,
38
+ model: Optional[str] = None,
39
+ ):
40
+ """Run the traditional console CLI interface"""
41
+ from minion_code.cli_simple import InterruptibleCLI
42
+
43
+ cli = InterruptibleCLI(
44
+ verbose=verbose,
45
+ mcp_config=mcp_config,
46
+ resume_session_id=resume_session_id,
47
+ continue_last=continue_last,
48
+ initial_prompt=initial_prompt,
49
+ print_output=print_output,
50
+ model=model,
51
+ )
52
+ return asyncio.run(cli.run())
53
+
54
+
55
+ def run_tui_repl(
56
+ debug: bool = False,
57
+ verbose: bool = False,
58
+ initial_prompt: Optional[str] = None,
59
+ dir: Optional[str] = None,
60
+ resume_session_id: Optional[str] = None,
61
+ continue_last: bool = False,
62
+ model: Optional[str] = None,
63
+ ):
64
+ """Run the modern TUI REPL interface"""
65
+ try:
66
+ from minion_code.screens.REPL import run
67
+
68
+ run(
69
+ initial_prompt=initial_prompt,
70
+ debug=debug,
71
+ verbose=verbose,
72
+ resume_session_id=resume_session_id,
73
+ continue_last=continue_last,
74
+ model=model,
114
75
  )
115
- self.console.print(success_panel)
116
-
117
- if self.verbose:
118
- self.console.print(f"[dim]Working directory: {os.getcwd()}[/dim]")
119
-
120
- def show_help(self):
121
- """Show help information."""
122
- help_table = Table(title="📚 MinionCode CLI Help", show_header=True, header_style="bold blue")
123
- help_table.add_column("Command/Key", style="cyan", no_wrap=True)
124
- help_table.add_column("Description", style="white")
125
-
126
- help_table.add_row("help", "Show this help")
127
- help_table.add_row("tools", "List available tools")
128
- help_table.add_row("history", "Show conversation history")
129
- help_table.add_row("clear", "Clear history")
130
- help_table.add_row("quit", "Exit")
131
- help_table.add_row("Ctrl+C", "Interrupt current task or exit")
132
-
133
- self.console.print(help_table)
134
- self.console.print("\n💡 [italic]Just type your message to chat with the AI agent![/italic]")
135
- self.console.print("⚠️ [italic]During task processing, press Ctrl+C to interrupt the current task[/italic]")
136
-
137
- async def process_input_with_interrupt(self, user_input: str):
138
- """Process user input with interrupt support."""
139
- self.task_cancelled = False
140
- self.interrupt_requested = False
141
-
142
- try:
143
- # Create the actual processing task
144
- async def processing_task():
145
- response = await self.agent.run_async(user_input)
146
- return response
147
-
148
- # Start the task
149
- self.current_task = asyncio.create_task(processing_task())
150
-
151
- # Monitor for cancellation while task runs
152
- while not self.current_task.done():
153
- if self.interrupt_requested:
154
- self.current_task.cancel()
155
- try:
156
- await self.current_task
157
- except asyncio.CancelledError:
158
- pass
159
- return None
160
-
161
- await asyncio.sleep(0.1) # Check every 100ms
162
-
163
- # Get the result
164
- response = await self.current_task
165
- return response
166
-
167
- except asyncio.CancelledError:
168
- return None
169
- finally:
170
- self.current_task = None
171
-
172
- def interrupt_current_task(self):
173
- """Interrupt the current running task."""
174
- if self.current_task and not self.current_task.done():
175
- self.interrupt_requested = True
176
- self.console.print("\n⚠️ [bold yellow]Task interruption requested...[/bold yellow]")
177
-
178
- async def cleanup(self):
179
- """Clean up resources."""
180
- if self.mcp_loader:
181
- try:
182
- await self.mcp_loader.close()
183
- except Exception as e:
184
- if self.verbose:
185
- self.console.print(f"[dim]Error during MCP cleanup: {e}[/dim]")
186
-
187
- async def process_input(self, user_input: str):
188
- """Process user input."""
189
- user_input = user_input.strip()
190
-
191
- if self.verbose:
192
- self.console.print(f"[dim]Processing input: {user_input[:50]}{'...' if len(user_input) > 50 else ''}[/dim]")
193
-
194
- # Check if it's a command (starts with /)
195
- if user_input.startswith('/'):
196
- await self.process_command(user_input)
197
- return
198
-
199
- # Process with agent
76
+ except ImportError as e:
77
+ console = Console()
78
+ console.print(f"❌ [bold red]TUI dependencies not available: {e}[/bold red]")
79
+ console.print(
80
+ "💡 [italic]Install TUI dependencies with: pip install textual rich[/italic]"
81
+ )
82
+ console.print("🔄 [italic]Falling back to console interface...[/italic]")
83
+ # Fallback to console CLI
84
+ run_console_cli(
85
+ verbose=verbose,
86
+ resume_session_id=resume_session_id,
87
+ continue_last=continue_last,
88
+ model=model,
89
+ )
90
+ except Exception as e:
91
+ console = Console()
92
+ console.print(f" [bold red]TUI error: {e}[/bold red]")
93
+ if verbose:
94
+ import traceback
95
+
96
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
97
+ raise typer.Exit(1)
98
+
99
+
100
+ @app.command()
101
+ def main(
102
+ prompt_arg: Optional[str] = typer.Argument(
103
+ None, help="Initial prompt to send to the agent (like 'claude \"prompt\"')"
104
+ ),
105
+ dir: Optional[str] = typer.Option(
106
+ None, "--dir", "-d", help="Change to specified directory before starting"
107
+ ),
108
+ model: Optional[str] = typer.Option(
109
+ None,
110
+ "--model",
111
+ "-m",
112
+ help="LLM model to use (e.g., gpt-4o, claude-3-5-sonnet). If not specified, uses config file setting.",
113
+ ),
114
+ verbose: bool = typer.Option(
115
+ False,
116
+ "--verbose",
117
+ "-v",
118
+ help="Enable verbose output with additional debugging information",
119
+ ),
120
+ debug: bool = typer.Option(
121
+ False, "--debug", help="Enable debug mode for development"
122
+ ),
123
+ prompt: Optional[str] = typer.Option(
124
+ None,
125
+ "--prompt",
126
+ "-p",
127
+ help="Initial prompt to send to the agent (alternative to positional arg)",
128
+ ),
129
+ console: bool = typer.Option(
130
+ False, "--console", help="Use console interface instead of TUI"
131
+ ),
132
+ config: Optional[str] = typer.Option(
133
+ None, "--config", "-c", help="Path to MCP configuration file (JSON format)"
134
+ ),
135
+ continue_session: bool = typer.Option(
136
+ False, "--continue", help="Continue the most recent session for this project"
137
+ ),
138
+ resume: Optional[str] = typer.Option(
139
+ None, "--resume", "-r", help="Resume a specific session by ID"
140
+ ),
141
+ print_output: bool = typer.Option(
142
+ False,
143
+ "--print",
144
+ help="Print output and exit (non-interactive mode, console only)",
145
+ ),
146
+ ):
147
+ """
148
+ 🤖 Start MinionCodeAgent - Modern AI-powered code assistant
149
+
150
+ By default starts the modern TUI REPL interface.
151
+ Use --console for traditional console interface.
152
+ """
153
+ # Change directory if specified
154
+ if dir:
200
155
  try:
201
- with Progress(
202
- SpinnerColumn(),
203
- TextColumn("[progress.description]{task.description} (Ctrl+C to interrupt)"),
204
- console=self.console,
205
- ) as progress:
206
- task = progress.add_task("🤖 Processing...", total=None)
207
-
208
- response = await self.process_input_with_interrupt(user_input)
209
-
210
- progress.update(task, completed=True)
211
-
212
- if response is None:
213
- # Task was cancelled
214
- cancelled_panel = Panel(
215
- "⚠️ [bold yellow]Task was interrupted![/bold yellow]",
216
- title="[bold yellow]Interrupted[/bold yellow]",
217
- border_style="yellow"
156
+ target_dir = Path(dir).resolve()
157
+ if not target_dir.exists():
158
+ console_obj = Console()
159
+ console_obj.print(
160
+ f"❌ [bold red]Directory does not exist: {dir}[/bold red]"
161
+ )
162
+ raise typer.Exit(1)
163
+ if not target_dir.is_dir():
164
+ console_obj = Console()
165
+ console_obj.print(
166
+ f"❌ [bold red]Path is not a directory: {dir}[/bold red]"
167
+ )
168
+ raise typer.Exit(1)
169
+
170
+ os.chdir(target_dir)
171
+ if verbose:
172
+ console_obj = Console()
173
+ console_obj.print(
174
+ f"📁 [bold green]Changed to directory: {target_dir}[/bold green]"
218
175
  )
219
- self.console.print(cancelled_panel)
220
- return
221
-
222
- # Display agent response with rich formatting
223
- if "```" in response.answer:
224
- agent_content = Markdown(response.answer)
225
- else:
226
- agent_content = response.answer
227
-
228
- response_panel = Panel(
229
- agent_content,
230
- title="🤖 [bold green]Agent Response[/bold green]",
231
- border_style="green"
232
- )
233
- self.console.print(response_panel)
234
-
235
- if self.verbose:
236
- self.console.print(f"[dim]Response length: {len(response.answer)} characters[/dim]")
237
-
238
- except KeyboardInterrupt:
239
- # Handle Ctrl+C during processing
240
- self.interrupt_current_task()
241
- cancelled_panel = Panel(
242
- "⚠️ [bold yellow]Task interrupted by user![/bold yellow]",
243
- title="[bold yellow]Interrupted[/bold yellow]",
244
- border_style="yellow"
245
- )
246
- self.console.print(cancelled_panel)
247
176
  except Exception as e:
248
- error_panel = Panel(
249
- f"❌ [bold red]Error: {e}[/bold red]",
250
- title="[bold red]Error[/bold red]",
251
- border_style="red"
177
+ console_obj = Console()
178
+ console_obj.print(
179
+ f"[bold red]Failed to change directory: {e}[/bold red]"
252
180
  )
253
- self.console.print(error_panel)
254
-
255
- if self.verbose:
256
- import traceback
257
- self.console.print(f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]")
258
-
259
- async def process_command(self, command_input: str):
260
- """Process a command input."""
261
- # Remove the leading /
262
- command_input = command_input[1:] if command_input.startswith('/') else command_input
263
-
264
- # Split command and arguments
265
- parts = command_input.split(' ', 1)
266
- command_name = parts[0].lower()
267
- args = parts[1] if len(parts) > 1 else ""
268
-
269
- if self.verbose:
270
- self.console.print(f"[dim]Executing command: {command_name} with args: {args}[/dim]")
271
-
272
- # Get command class
273
- command_class = command_registry.get_command(command_name)
274
- if not command_class:
275
- error_panel = Panel(
276
- f"❌ [bold red]Unknown command: /{command_name}[/bold red]\n"
277
- f"💡 [italic]Use '/help' to see available commands[/italic]",
278
- title="[bold red]Error[/bold red]",
279
- border_style="red"
280
- )
281
- self.console.print(error_panel)
282
- return
283
-
284
- # Create and execute command
285
- try:
286
- command_instance = command_class(self.console, self.agent)
287
-
288
- # Special handling for quit command
289
- if command_name in ["quit", "exit", "q", "bye"]:
290
- command_instance._tui_instance = self
291
-
292
- await command_instance.execute(args)
293
-
294
- except Exception as e:
295
- error_panel = Panel(
296
- f"❌ [bold red]Error executing command /{command_name}: {e}[/bold red]",
297
- title="[bold red]Command Error[/bold red]",
298
- border_style="red"
181
+ raise typer.Exit(1)
182
+
183
+ # Validate MCP config if provided
184
+ mcp_config_path = None
185
+ if config:
186
+ mcp_config_path = Path(config).resolve()
187
+ if not mcp_config_path.exists():
188
+ console_obj = Console()
189
+ console_obj.print(
190
+ f"❌ [bold red]MCP config file does not exist: {config}[/bold red]"
299
191
  )
300
- self.console.print(error_panel)
301
-
302
- if self.verbose:
303
- import traceback
304
- self.console.print(f"[dim]Full traceback:\n{traceback.format_exc()}[/dim]")
305
-
306
- def show_tools(self):
307
- """Show available tools in a beautiful table."""
308
- if not self.agent or not self.agent.tools:
309
- self.console.print("❌ No tools available")
310
- return
311
-
312
- tools_table = Table(title="🛠️ Available Tools", show_header=True, header_style="bold magenta")
313
- tools_table.add_column("Tool Name", style="cyan", no_wrap=True)
314
- tools_table.add_column("Description", style="white")
315
- tools_table.add_column("Source", style="yellow")
316
- tools_table.add_column("Type", style="green")
317
-
318
- # Separate MCP tools from built-in tools
319
- mcp_tool_names = {tool.name for tool in self.mcp_tools} if self.mcp_tools else set()
320
-
321
- for tool in self.agent.tools:
322
- tool_type = "Read-only" if getattr(tool, 'readonly', False) else "Read-write"
323
- source = "MCP" if tool.name in mcp_tool_names else "Built-in"
324
-
325
- tools_table.add_row(
326
- tool.name,
327
- tool.description[:60] + "..." if len(tool.description) > 60 else tool.description,
328
- source,
329
- tool_type
192
+ raise typer.Exit(1)
193
+ if not mcp_config_path.is_file():
194
+ console_obj = Console()
195
+ console_obj.print(
196
+ f"[bold red]MCP config path is not a file: {config}[/bold red]"
330
197
  )
331
-
332
- self.console.print(tools_table)
333
-
334
- # Show summary
335
- total_tools = len(self.agent.tools)
336
- mcp_count = len(self.mcp_tools) if self.mcp_tools else 0
337
- builtin_count = total_tools - mcp_count
338
-
339
- summary_text = f"[dim]Total: {total_tools} tools"
340
- if mcp_count > 0:
341
- summary_text += f" (Built-in: {builtin_count}, MCP: {mcp_count})"
342
- summary_text += "[/dim]"
343
-
344
- self.console.print(summary_text)
345
-
346
- def show_history(self):
347
- """Show conversation history in a beautiful format."""
348
- if not self.agent:
349
- return
350
-
351
- history = self.agent.get_conversation_history()
352
- if not history:
353
- no_history_panel = Panel(
354
- "📝 [italic]No conversation history yet.[/italic]",
355
- title="[bold blue]History[/bold blue]",
356
- border_style="blue"
198
+ raise typer.Exit(1)
199
+
200
+ if verbose:
201
+ console_obj = Console()
202
+ console_obj.print(
203
+ f"🔌 [bold green]Using MCP config: {mcp_config_path}[/bold green]"
357
204
  )
358
- self.console.print(no_history_panel)
359
- return
360
-
361
- history_panel = Panel(
362
- f"📝 [bold blue]Conversation History ({len(history)} messages)[/bold blue]",
363
- border_style="blue"
205
+
206
+ # Combine prompt sources (positional arg takes precedence)
207
+ initial_prompt = prompt_arg or prompt
208
+
209
+ # Choose interface based on flags
210
+ if console:
211
+ # Use console interface
212
+ run_console_cli(
213
+ verbose=verbose,
214
+ mcp_config=mcp_config_path,
215
+ resume_session_id=resume,
216
+ continue_last=continue_session,
217
+ initial_prompt=initial_prompt,
218
+ print_output=print_output,
219
+ model=model,
364
220
  )
365
- self.console.print(history_panel)
366
-
367
- display_count = 5 if not self.verbose else 10
368
- for i, entry in enumerate(history[-display_count:], 1):
369
- # User message
370
- user_panel = Panel(
371
- entry['user_message'][:200] + "..." if len(entry['user_message']) > 200 else entry['user_message'],
372
- title=f"👤 [bold cyan]You (Message {len(history)-display_count+i})[/bold cyan]",
373
- border_style="cyan"
374
- )
375
- self.console.print(user_panel)
376
-
377
- # Agent response
378
- agent_response = entry['agent_response'][:200] + "..." if len(entry['agent_response']) > 200 else entry['agent_response']
379
- agent_panel = Panel(
380
- agent_response,
381
- title="🤖 [bold green]Agent[/bold green]",
382
- border_style="green"
383
- )
384
- self.console.print(agent_panel)
385
- self.console.print() # Add spacing
386
-
387
- async def run(self):
388
- """Run the CLI."""
389
- # Welcome banner
390
- welcome_panel = Panel(
391
- "🚀 [bold blue]MinionCodeAgent CLI[/bold blue]\n"
392
- "💡 [italic]Use '/help' for commands or just chat with the agent![/italic]\n"
393
- "⚠️ [italic]Press Ctrl+C during processing to interrupt tasks[/italic]\n"
394
- "🛑 [italic]Type '/quit' to exit[/italic]",
395
- title="[bold magenta]Welcome[/bold magenta]",
396
- border_style="magenta"
221
+ else:
222
+ # Use TUI interface (default)
223
+ run_tui_repl(
224
+ debug=debug,
225
+ verbose=verbose,
226
+ initial_prompt=initial_prompt,
227
+ dir=dir,
228
+ resume_session_id=resume,
229
+ continue_last=continue_session,
230
+ model=model,
397
231
  )
398
- self.console.print(welcome_panel)
399
-
400
- await self.setup()
401
-
402
- while self.running:
403
- try:
404
- # Use rich prompt for better input experience
405
- user_input = Prompt.ask(
406
- "\n[bold cyan]👤 You[/bold cyan]",
407
- console=self.console
408
- ).strip()
409
-
410
- if user_input:
411
- await self.process_input(user_input)
412
-
413
- except (EOFError, KeyboardInterrupt):
414
- # Handle Ctrl+C at input prompt
415
- if self.current_task and not self.current_task.done():
416
- # If there's a running task, interrupt it
417
- self.interrupt_current_task()
418
- else:
419
- # If no running task, exit
420
- goodbye_panel = Panel(
421
- "\n👋 [bold yellow]Goodbye![/bold yellow]",
422
- title="[bold red]Exit[/bold red]",
423
- border_style="red"
424
- )
425
- self.console.print(goodbye_panel)
426
- break
427
-
428
- # Cleanup resources
429
- await self.cleanup()
430
232
 
431
233
 
432
234
  @app.command()
433
- def main(
235
+ def repl(
434
236
  dir: Optional[str] = typer.Option(
237
+ None, "--dir", "-d", help="🗂️ Change to specified directory before starting"
238
+ ),
239
+ model: Optional[str] = typer.Option(
435
240
  None,
436
- "--dir",
437
- "-d",
438
- help="🗂️ Change to specified directory before starting"
241
+ "--model",
242
+ "-m",
243
+ help="🤖 LLM model to use (e.g., gpt-4o, claude-3-5-sonnet)",
439
244
  ),
440
245
  verbose: bool = typer.Option(
441
246
  False,
442
247
  "--verbose",
443
248
  "-v",
444
- help="🔍 Enable verbose output with additional debugging information"
249
+ help="🔍 Enable verbose output with additional debugging information",
445
250
  ),
446
- config: Optional[str] = typer.Option(
447
- None,
448
- "--config",
449
- "-c",
450
- help="🔌 Path to MCP configuration file (JSON format)"
251
+ debug: bool = typer.Option(
252
+ False, "--debug", help="🐛 Enable debug mode for development"
253
+ ),
254
+ prompt: Optional[str] = typer.Option(
255
+ None, "--prompt", "-p", help="💬 Initial prompt to send to the agent"
256
+ ),
257
+ ):
258
+ """
259
+ 🖥️ Start the REPL (Read-Eval-Print Loop) TUI interface
260
+
261
+ A modern terminal interface with streaming responses and interactive features.
262
+ """
263
+ # Change directory if specified
264
+ if dir:
265
+ try:
266
+ target_dir = Path(dir).resolve()
267
+ if not target_dir.exists():
268
+ console_obj = Console()
269
+ console_obj.print(
270
+ f"❌ [bold red]Directory does not exist: {dir}[/bold red]"
271
+ )
272
+ raise typer.Exit(1)
273
+ if not target_dir.is_dir():
274
+ console_obj = Console()
275
+ console_obj.print(
276
+ f"❌ [bold red]Path is not a directory: {dir}[/bold red]"
277
+ )
278
+ raise typer.Exit(1)
279
+
280
+ os.chdir(target_dir)
281
+ if verbose:
282
+ console_obj = Console()
283
+ console_obj.print(
284
+ f"📁 [bold green]Changed to directory: {target_dir}[/bold green]"
285
+ )
286
+ except Exception as e:
287
+ console_obj = Console()
288
+ console_obj.print(
289
+ f"❌ [bold red]Failed to change directory: {e}[/bold red]"
290
+ )
291
+ raise typer.Exit(1)
292
+
293
+ # Run TUI REPL
294
+ run_tui_repl(
295
+ debug=debug, verbose=verbose, initial_prompt=prompt, dir=dir, model=model
451
296
  )
297
+
298
+
299
+ @app.command()
300
+ def console(
301
+ dir: Optional[str] = typer.Option(
302
+ None, "--dir", "-d", help="Change to specified directory before starting"
303
+ ),
304
+ model: Optional[str] = typer.Option(
305
+ None, "--model", "-m", help="LLM model to use (e.g., gpt-4o, claude-3-5-sonnet)"
306
+ ),
307
+ verbose: bool = typer.Option(
308
+ False,
309
+ "--verbose",
310
+ "-v",
311
+ help="Enable verbose output with additional debugging information",
312
+ ),
313
+ config: Optional[str] = typer.Option(
314
+ None, "--config", "-c", help="Path to MCP configuration file (JSON format)"
315
+ ),
316
+ continue_session: bool = typer.Option(
317
+ False, "--continue", help="Continue the most recent session for this project"
318
+ ),
319
+ resume: Optional[str] = typer.Option(
320
+ None, "--resume", "-r", help="Resume a specific session by ID"
321
+ ),
452
322
  ):
453
323
  """
454
- 🤖 Start the MinionCodeAgent CLI interface
455
-
456
- An AI-powered code assistant with task interruption support and MCP tools integration.
324
+ 🖥️ Start the traditional console CLI interface
325
+
326
+ Console-based interface for environments without TUI support.
457
327
  """
458
- console = Console()
459
-
460
328
  # Change directory if specified
461
329
  if dir:
462
330
  try:
463
331
  target_dir = Path(dir).resolve()
464
332
  if not target_dir.exists():
465
- console.print(f"❌ [bold red]Directory does not exist: {dir}[/bold red]")
333
+ console_obj = Console()
334
+ console_obj.print(
335
+ f"❌ [bold red]Directory does not exist: {dir}[/bold red]"
336
+ )
466
337
  raise typer.Exit(1)
467
338
  if not target_dir.is_dir():
468
- console.print(f"❌ [bold red]Path is not a directory: {dir}[/bold red]")
339
+ console_obj = Console()
340
+ console_obj.print(
341
+ f"❌ [bold red]Path is not a directory: {dir}[/bold red]"
342
+ )
469
343
  raise typer.Exit(1)
470
-
344
+
471
345
  os.chdir(target_dir)
472
346
  if verbose:
473
- console.print(f"📁 [bold green]Changed to directory: {target_dir}[/bold green]")
347
+ console_obj = Console()
348
+ console_obj.print(
349
+ f"📁 [bold green]Changed to directory: {target_dir}[/bold green]"
350
+ )
474
351
  except Exception as e:
475
- console.print(f"❌ [bold red]Failed to change directory: {e}[/bold red]")
352
+ console_obj = Console()
353
+ console_obj.print(
354
+ f"❌ [bold red]Failed to change directory: {e}[/bold red]"
355
+ )
476
356
  raise typer.Exit(1)
477
-
357
+
478
358
  # Validate MCP config if provided
479
359
  mcp_config_path = None
480
360
  if config:
481
361
  mcp_config_path = Path(config).resolve()
482
362
  if not mcp_config_path.exists():
483
- console.print(f"❌ [bold red]MCP config file does not exist: {config}[/bold red]")
363
+ console_obj = Console()
364
+ console_obj.print(
365
+ f"❌ [bold red]MCP config file does not exist: {config}[/bold red]"
366
+ )
484
367
  raise typer.Exit(1)
485
368
  if not mcp_config_path.is_file():
486
- console.print(f"❌ [bold red]MCP config path is not a file: {config}[/bold red]")
369
+ console_obj = Console()
370
+ console_obj.print(
371
+ f"❌ [bold red]MCP config path is not a file: {config}[/bold red]"
372
+ )
487
373
  raise typer.Exit(1)
488
-
374
+
489
375
  if verbose:
490
- console.print(f"🔌 [bold green]Using MCP config: {mcp_config_path}[/bold green]")
491
-
492
- # Create and run CLI
493
- cli = InterruptibleCLI(verbose=verbose, mcp_config=mcp_config_path)
494
-
495
- try:
496
- asyncio.run(cli.run())
497
- except KeyboardInterrupt:
498
- console.print("\n👋 [bold yellow]Goodbye![/bold yellow]")
376
+ console_obj = Console()
377
+ console_obj.print(
378
+ f"🔌 [bold green]Using MCP config: {mcp_config_path}[/bold green]"
379
+ )
380
+
381
+ # Run console CLI
382
+ run_console_cli(
383
+ verbose=verbose,
384
+ mcp_config=mcp_config_path,
385
+ resume_session_id=resume,
386
+ continue_last=continue_session,
387
+ model=model,
388
+ )
389
+
390
+
391
+ @app.command(name="model")
392
+ def model_cmd(
393
+ model_name: Optional[str] = typer.Argument(
394
+ None,
395
+ help="Model name to set (e.g., gpt-4o, claude-3-5-sonnet). If not provided, shows current model.",
396
+ ),
397
+ clear: bool = typer.Option(
398
+ False, "--clear", "-c", help="Clear the saved model (use default)"
399
+ ),
400
+ ):
401
+ """
402
+ 🤖 Configure the default LLM model.
403
+
404
+ Set, view, or clear the default model used by minion-code.
405
+ The model setting is saved to ~/.minion/minion-code.json.
406
+
407
+ Examples:
408
+ # View current model
409
+ mcode model
410
+
411
+ # Set default model
412
+ mcode model gpt-4o
413
+ mcode model claude-3-5-sonnet
414
+
415
+ # Clear model (use default)
416
+ mcode model --clear
417
+ """
418
+ import json
419
+ from pathlib import Path
420
+ from rich.console import Console
421
+
422
+ console_obj = Console()
423
+ config_dir = Path.home() / ".minion"
424
+ config_file = config_dir / "minion-code.json"
425
+
426
+ # Load current config
427
+ config = {}
428
+ if config_file.exists():
429
+ try:
430
+ with open(config_file, "r") as f:
431
+ config = json.load(f)
432
+ except Exception:
433
+ pass
434
+
435
+ if clear:
436
+ # Clear model setting
437
+ if "model" in config:
438
+ del config["model"]
439
+ config_dir.mkdir(parents=True, exist_ok=True)
440
+ with open(config_file, "w") as f:
441
+ json.dump(config, f, indent=2)
442
+ console_obj.print(
443
+ "✅ [green]Model setting cleared. Will use default model.[/green]"
444
+ )
445
+ else:
446
+ console_obj.print("ℹ️ [dim]No model setting to clear.[/dim]")
447
+ elif model_name:
448
+ # Set model
449
+ config["model"] = model_name
450
+ config_dir.mkdir(parents=True, exist_ok=True)
451
+ with open(config_file, "w") as f:
452
+ json.dump(config, f, indent=2)
453
+ console_obj.print(
454
+ f"✅ [green]Default model set to:[/green] [bold cyan]{model_name}[/bold cyan]"
455
+ )
456
+ console_obj.print(f"📁 [dim]Config saved to: {config_file}[/dim]")
457
+ else:
458
+ # Show current model
459
+ current_model = config.get("model")
460
+ if current_model:
461
+ console_obj.print(
462
+ f"🤖 [bold]Current default model:[/bold] [cyan]{current_model}[/cyan]"
463
+ )
464
+ else:
465
+ console_obj.print(
466
+ "🤖 [bold]No default model set[/bold] (using built-in default)"
467
+ )
468
+ console_obj.print(f"\n💡 [dim]Set with: mcode model <model-name>[/dim]")
469
+ console_obj.print(f"💡 [dim]Clear with: mcode model --clear[/dim]")
470
+
471
+
472
+ @app.command()
473
+ def serve(
474
+ host: str = typer.Option(
475
+ "0.0.0.0", "--host", "-H", help="Host to bind the server to"
476
+ ),
477
+ port: int = typer.Option(8000, "--port", "-p", help="Port to listen on"),
478
+ reload: bool = typer.Option(
479
+ False, "--reload", "-r", help="Enable auto-reload for development"
480
+ ),
481
+ log_level: str = typer.Option(
482
+ "info", "--log-level", "-l", help="Logging level (debug, info, warning, error)"
483
+ ),
484
+ ):
485
+ """
486
+ 🌐 Start the Web API server.
487
+
488
+ Provides HTTP/SSE API for cross-process frontend communication.
489
+ Similar to claude.ai, uses Server-Sent Events for streaming responses.
490
+
491
+ Examples:
492
+ # Start server on default port (8000)
493
+ mcode serve
494
+
495
+ # Start on custom port with auto-reload
496
+ mcode serve --port 3001 --reload
497
+
498
+ # Production mode with specific host
499
+ mcode serve --host 127.0.0.1 --port 8080
500
+ """
501
+ from rich.console import Console
502
+
503
+ console_obj = Console()
504
+
505
+ console_obj.print(f"🌐 [bold green]Starting Minion Code Web API[/bold green]")
506
+ console_obj.print(f"📡 Server: http://{host}:{port}")
507
+ console_obj.print(f"📚 API Docs: http://{host}:{port}/docs")
508
+ console_obj.print()
509
+
510
+ from minion_code.web.server import run_server
511
+
512
+ run_server(host=host, port=port, reload=reload, log_level=log_level)
513
+
514
+
515
+ @app.command()
516
+ def acp(
517
+ directory: Optional[str] = typer.Option(
518
+ None, "--dir", "-d", help="Working directory for the agent"
519
+ ),
520
+ model: Optional[str] = typer.Option(
521
+ None,
522
+ "--model",
523
+ "-m",
524
+ help="LLM model to use (e.g., gpt-4o, claude-3-5-sonnet). If not specified, uses config file setting.",
525
+ ),
526
+ verbose: bool = typer.Option(
527
+ False, "--verbose", "-v", help="Enable verbose (debug) logging"
528
+ ),
529
+ log_level: str = typer.Option(
530
+ "info", "--log-level", "-l", help="Logging level (debug, info, warning, error)"
531
+ ),
532
+ dangerously_skip_permissions: bool = typer.Option(
533
+ False,
534
+ "--dangerously-skip-permissions",
535
+ help="Skip permission prompts for tool calls (dangerous!)",
536
+ ),
537
+ ):
538
+ """
539
+ 🔌 Start as ACP (Agent Client Protocol) agent.
540
+
541
+ Runs minion-code as an ACP-compatible agent over stdio.
542
+ This allows integration with ACP clients like Zed editor.
543
+
544
+ The agent communicates via JSON-RPC over stdin/stdout,
545
+ with all other output redirected to stderr.
546
+
547
+ Examples:
548
+ # Start ACP agent
549
+ mcode acp
550
+
551
+ # Start with specific working directory
552
+ mcode acp --dir /path/to/project
553
+
554
+ # Start with specific model
555
+ mcode acp --model gpt-4o
556
+
557
+ # Start with verbose logging
558
+ mcode acp --verbose
559
+
560
+ # Start without permission prompts (dangerous!)
561
+ mcode acp --dangerously-skip-permissions
562
+ """
563
+ # Handle verbose flag
564
+ if verbose:
565
+ log_level = "debug"
566
+
567
+ from minion_code.acp_server.main import main as acp_main
568
+
569
+ acp_main(
570
+ log_level=log_level,
571
+ dangerously_skip_permissions=dangerously_skip_permissions,
572
+ cwd=directory,
573
+ model=model,
574
+ )
575
+
576
+
577
+ def run():
578
+ """Entry point for pyproject.toml scripts."""
579
+ _maybe_insert_main_command()
580
+ app()
581
+
582
+
583
+ def _maybe_insert_main_command():
584
+ """Insert 'main' command if not provided, to enable 'mcode "prompt"' usage."""
585
+ known_commands = {
586
+ "main",
587
+ "repl",
588
+ "console",
589
+ "serve",
590
+ "acp",
591
+ "model",
592
+ "--help",
593
+ "-h",
594
+ }
595
+ args = sys.argv[1:]
596
+
597
+ if not args:
598
+ # No args, insert 'main'
599
+ sys.argv.insert(1, "main")
600
+ elif args[0] in ("--help", "-h"):
601
+ # Help requested, don't modify
602
+ pass
603
+ elif args[0] not in known_commands:
604
+ # First arg is not a known command - could be a prompt or option
605
+ # Insert 'main' to treat it as: mcode main "prompt" or mcode main --option
606
+ sys.argv.insert(1, "main")
499
607
 
500
608
 
501
609
  if __name__ == "__main__":
502
- app()
610
+ _maybe_insert_main_command()
611
+ app()