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