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
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Hook system for MinionCodeAgent.
5
+
6
+ Re-exports base hooks from minion framework and provides
7
+ minion-code specific hook implementations.
8
+ """
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional, Union, Callable, Set
12
+
13
+ # Re-export base types from minion framework
14
+ from minion.main.tool_hooks import (
15
+ PermissionDecision,
16
+ PreToolUseResult,
17
+ PostToolUseResult,
18
+ ToolCallInfo,
19
+ PreToolUseHook,
20
+ PostToolUseHook,
21
+ HookMatcher,
22
+ PostHookMatcher,
23
+ HookConfig,
24
+ ToolHooks,
25
+ NoOpToolHooks,
26
+ HookedTool,
27
+ wrap_tools_with_hooks,
28
+ create_auto_accept_hook,
29
+ create_auto_deny_hook,
30
+ create_dangerous_command_check_hook,
31
+ create_logging_hook,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ # ============================================================================
38
+ # Spinner Controller for CLI
39
+ # ============================================================================
40
+
41
+
42
+ class SpinnerController:
43
+ """
44
+ Controller to pause/resume spinners during user prompts.
45
+
46
+ Usage:
47
+ controller = SpinnerController()
48
+ hook = create_cli_confirm_hook(spinner_controller=controller)
49
+
50
+ # In your main loop:
51
+ with Progress(...) as progress:
52
+ controller.set_progress(progress)
53
+ # ... run agent ...
54
+ controller.clear_progress()
55
+ """
56
+
57
+ def __init__(self):
58
+ self._progress = None
59
+ self._is_paused = False
60
+
61
+ def set_progress(self, progress: Any) -> None:
62
+ """Set the current Progress instance."""
63
+ self._progress = progress
64
+ self._is_paused = False
65
+
66
+ def clear_progress(self) -> None:
67
+ """Clear the current Progress instance."""
68
+ self._progress = None
69
+ self._is_paused = False
70
+
71
+ def pause(self) -> None:
72
+ """Pause the spinner (stop the progress display)."""
73
+ if self._progress and not self._is_paused:
74
+ try:
75
+ self._progress.stop()
76
+ self._is_paused = True
77
+ except Exception as e:
78
+ logger.debug(f"Failed to pause spinner: {e}")
79
+
80
+ def resume(self) -> None:
81
+ """Resume the spinner (start the progress display)."""
82
+ if self._progress and self._is_paused:
83
+ try:
84
+ self._progress.start()
85
+ self._is_paused = False
86
+ except Exception as e:
87
+ logger.debug(f"Failed to resume spinner: {e}")
88
+
89
+
90
+ # ============================================================================
91
+ # minion-code Specific Hook Implementations
92
+ # ============================================================================
93
+
94
+
95
+ def _format_tool_input(tool_name: str, tool_input: Dict[str, Any]) -> str:
96
+ """Format tool input for display in confirmation dialog."""
97
+ if tool_name == "bash":
98
+ return f"Command: {tool_input.get('command', '')}"
99
+
100
+ if tool_name in ("file_write", "file_edit"):
101
+ path = tool_input.get("file_path", tool_input.get("path", ""))
102
+ return f"File: {path}"
103
+
104
+ if tool_name == "file_read":
105
+ return f"File: {tool_input.get('file_path', '')}"
106
+
107
+ if tool_name == "Task":
108
+ desc = tool_input.get("description", "")
109
+ prompt = tool_input.get("prompt", "")
110
+ subagent = tool_input.get("subagent_type", "general-purpose")
111
+ # Truncate prompt if too long
112
+ if len(prompt) > 200:
113
+ prompt = prompt[:200] + "..."
114
+ return f"Subagent: {subagent}\nDescription: {desc}\nPrompt: {prompt}"
115
+
116
+ # Default: show all parameters (excluding internal ones like 'state')
117
+ parts = []
118
+ skip_keys = {"state"} # Internal parameters to hide
119
+ for key, value in tool_input.items():
120
+ if key in skip_keys:
121
+ continue
122
+ if isinstance(value, str) and len(value) > 100:
123
+ value = value[:100] + "..."
124
+ parts.append(f"{key}: {value}")
125
+
126
+ return "\n".join(parts) if parts else "(no parameters)"
127
+
128
+
129
+ def create_confirm_writes_hook(
130
+ adapter: Any, # OutputAdapter
131
+ tools_registry: Optional[Dict[str, Any]] = None,
132
+ skip_readonly: bool = True,
133
+ ) -> PreToolUseHook:
134
+ """
135
+ Create a hook that requests user confirmation for non-readonly tools.
136
+
137
+ Args:
138
+ adapter: OutputAdapter instance with confirm() method
139
+ tools_registry: Optional dict mapping tool names to tool instances
140
+ skip_readonly: If True, auto-accept readonly tools without confirmation
141
+ """
142
+
143
+ async def confirm_writes(
144
+ tool_name: str, tool_input: Dict[str, Any], tool_use_id: str
145
+ ) -> PreToolUseResult:
146
+ # Skip readonly tools
147
+ if skip_readonly:
148
+ # Known readonly tools (hardcoded list)
149
+ # Task is readonly because subagents have their own permission control
150
+ readonly_tools = {
151
+ "file_read",
152
+ "glob",
153
+ "grep",
154
+ "ls",
155
+ "web_fetch",
156
+ "web_search",
157
+ "todo_read",
158
+ "Task",
159
+ }
160
+ if tool_name in readonly_tools:
161
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
162
+
163
+ # Check if tool is readonly from registry
164
+ if tools_registry:
165
+ tool = tools_registry.get(tool_name)
166
+ if tool and getattr(tool, "readonly", False):
167
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
168
+
169
+ # Format tool input for display
170
+ input_summary = _format_tool_input(tool_name, tool_input)
171
+
172
+ # Request confirmation
173
+ try:
174
+ confirmed = await adapter.confirm(
175
+ message=f"Allow {tool_name}?\n{input_summary}",
176
+ title="Tool Permission",
177
+ resource_type="tool",
178
+ resource_name=tool_name,
179
+ resource_args=tool_input,
180
+ )
181
+ except Exception as e:
182
+ logger.error(f"Error during confirmation: {e}")
183
+ return PreToolUseResult(
184
+ decision=PermissionDecision.DENY, reason=f"Confirmation error: {e}"
185
+ )
186
+
187
+ if confirmed:
188
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
189
+ else:
190
+ return PreToolUseResult(
191
+ decision=PermissionDecision.DENY, reason="User denied permission"
192
+ )
193
+
194
+ return confirm_writes
195
+
196
+
197
+ def create_cli_confirm_hook(
198
+ allowed_tools: Optional[Set[str]] = None,
199
+ session_allowed: Optional[Set[str]] = None,
200
+ console: Optional[Any] = None,
201
+ spinner_controller: Optional[SpinnerController] = None,
202
+ ) -> PreToolUseHook:
203
+ """
204
+ Create a hook that prompts for confirmation in CLI/terminal.
205
+
206
+ Args:
207
+ allowed_tools: Set of tool names that are always allowed (persistent)
208
+ session_allowed: Set of tool names allowed for this session only
209
+ console: Optional Rich Console for better formatting
210
+ spinner_controller: Optional SpinnerController to pause spinner during prompts
211
+ """
212
+ if allowed_tools is None:
213
+ allowed_tools = set()
214
+ if session_allowed is None:
215
+ session_allowed = set()
216
+
217
+ # Task is readonly because subagents have their own permission control
218
+ readonly_tools = {
219
+ "file_read",
220
+ "glob",
221
+ "grep",
222
+ "ls",
223
+ "web_fetch",
224
+ "web_search",
225
+ "todo_read",
226
+ "Task",
227
+ }
228
+
229
+ # Use Rich console if provided, otherwise create one
230
+ if console is None:
231
+ from rich.console import Console
232
+
233
+ console = Console()
234
+
235
+ async def cli_confirm(
236
+ tool_name: str, tool_input: Dict[str, Any], tool_use_id: str
237
+ ) -> PreToolUseResult:
238
+ # Skip readonly tools
239
+ if tool_name in readonly_tools:
240
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
241
+
242
+ if tool_name in allowed_tools or tool_name in session_allowed:
243
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
244
+
245
+ input_summary = _format_tool_input(tool_name, tool_input)
246
+
247
+ # Pause spinner before prompting user
248
+ if spinner_controller:
249
+ spinner_controller.pause()
250
+
251
+ try:
252
+ # Use Rich console to print
253
+ console.print()
254
+ console.print(f"[bold yellow]{'='*60}[/]")
255
+ console.print(f"[bold cyan]🔧 Tool: {tool_name}[/]")
256
+ console.print(f"[bold yellow]{'='*60}[/]")
257
+ console.print(input_summary)
258
+ console.print(f"[bold yellow]{'='*60}[/]")
259
+ console.print("[dim]Options:[/]")
260
+ console.print(" [green][y][/] Yes, allow this once")
261
+ console.print(" [red][n][/] No, deny")
262
+ console.print(" [yellow][a][/] Always allow this tool (session)")
263
+ console.print(" [blue][A][/] Always allow this tool (permanent)")
264
+
265
+ try:
266
+ # Don't lowercase - we need to distinguish 'a' from 'A'
267
+ response = console.input("[bold]Allow? [y/n/a/A]: [/]").strip()
268
+ except (EOFError, KeyboardInterrupt):
269
+ return PreToolUseResult(
270
+ decision=PermissionDecision.DENY, reason="User cancelled"
271
+ )
272
+
273
+ if response.lower() == "y":
274
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
275
+ elif response == "a": # Session allow (lowercase only)
276
+ session_allowed.add(tool_name)
277
+ logger.info(f"Tool '{tool_name}' allowed for this session")
278
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
279
+ elif response == "A": # Permanent allow (uppercase only)
280
+ allowed_tools.add(tool_name)
281
+ logger.info(f"Tool '{tool_name}' permanently allowed")
282
+ return PreToolUseResult(decision=PermissionDecision.ACCEPT)
283
+ else:
284
+ return PreToolUseResult(
285
+ decision=PermissionDecision.DENY, reason="User denied permission"
286
+ )
287
+ finally:
288
+ # Resume spinner after user responds
289
+ if spinner_controller:
290
+ spinner_controller.resume()
291
+
292
+ return cli_confirm
293
+
294
+
295
+ def create_cli_hooks(
296
+ auto_accept: bool = False,
297
+ spinner_controller: Optional[SpinnerController] = None,
298
+ console: Optional[Any] = None,
299
+ ) -> HookConfig:
300
+ """
301
+ Create hook configuration for CLI usage.
302
+
303
+ Args:
304
+ auto_accept: If True, auto-accept all tools (no confirmation prompts)
305
+ spinner_controller: Optional SpinnerController to pause spinner during prompts
306
+ console: Optional Rich Console for output
307
+ """
308
+ if auto_accept:
309
+ return create_autonomous_hooks()
310
+
311
+ return HookConfig(
312
+ pre_tool_use=[
313
+ HookMatcher("bash", create_dangerous_command_check_hook()),
314
+ HookMatcher(
315
+ "*",
316
+ create_cli_confirm_hook(
317
+ spinner_controller=spinner_controller,
318
+ console=console,
319
+ ),
320
+ ),
321
+ ]
322
+ )
323
+
324
+
325
+ def create_default_hooks(adapter: Any) -> HookConfig:
326
+ """
327
+ Create default hook configuration with:
328
+ - Dangerous command blocking for bash
329
+ - User confirmation for write operations
330
+ """
331
+ return HookConfig(
332
+ pre_tool_use=[
333
+ HookMatcher("bash", create_dangerous_command_check_hook()),
334
+ HookMatcher("*", create_confirm_writes_hook(adapter)),
335
+ ]
336
+ )
337
+
338
+
339
+ def create_autonomous_hooks() -> HookConfig:
340
+ """
341
+ Create hook configuration for autonomous/unattended mode.
342
+ Blocks dangerous commands but auto-accepts everything else.
343
+ """
344
+ return HookConfig(
345
+ pre_tool_use=[
346
+ HookMatcher("bash", create_dangerous_command_check_hook()),
347
+ HookMatcher("*", create_auto_accept_hook()),
348
+ ]
349
+ )
350
+
351
+
352
+ __all__ = [
353
+ # Re-exported from minion.main.tool_hooks
354
+ "PermissionDecision",
355
+ "PreToolUseResult",
356
+ "PostToolUseResult",
357
+ "ToolCallInfo",
358
+ "PreToolUseHook",
359
+ "PostToolUseHook",
360
+ "HookMatcher",
361
+ "PostHookMatcher",
362
+ "HookConfig",
363
+ "ToolHooks",
364
+ "NoOpToolHooks",
365
+ "HookedTool",
366
+ "wrap_tools_with_hooks",
367
+ "create_auto_accept_hook",
368
+ "create_auto_deny_hook",
369
+ "create_dangerous_command_check_hook",
370
+ "create_logging_hook",
371
+ # minion-code specific
372
+ "SpinnerController",
373
+ "create_confirm_writes_hook",
374
+ "create_cli_confirm_hook",
375
+ "create_cli_hooks",
376
+ "create_default_hooks",
377
+ "create_autonomous_hooks",
378
+ ]