minion-code 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.1.dist-info/METADATA +475 -0
  98. minion_code-0.1.1.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.1.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,114 @@
1
+ """History management utilities for minion-code.
2
+
3
+ This module provides command history functionality similar to the TypeScript
4
+ history.ts file, adapted for Python and the minion-code project structure.
5
+ """
6
+
7
+ from typing import List
8
+ from .config import get_current_project_config, save_current_project_config
9
+
10
+ MAX_HISTORY_ITEMS = 100
11
+
12
+
13
+ def get_history() -> List[str]:
14
+ """Get command history for the current project.
15
+
16
+ Returns:
17
+ List of command history strings, with most recent first.
18
+ """
19
+ project_config = get_current_project_config()
20
+ return project_config.history or []
21
+
22
+
23
+ def add_to_history(command: str) -> None:
24
+ """Add a command to the history.
25
+
26
+ Args:
27
+ command: The command string to add to history.
28
+
29
+ Note:
30
+ - Commands are added to the beginning of the history list
31
+ - Duplicate consecutive commands are not added
32
+ - History is limited to MAX_HISTORY_ITEMS entries
33
+ """
34
+ if not command or not command.strip():
35
+ return
36
+
37
+ command = command.strip()
38
+ project_config = get_current_project_config()
39
+ history = project_config.history or []
40
+
41
+ # Don't add if it's the same as the most recent command
42
+ if history and history[0] == command:
43
+ return
44
+
45
+ # Add to beginning and limit to MAX_HISTORY_ITEMS
46
+ history.insert(0, command)
47
+ project_config.history = history[:MAX_HISTORY_ITEMS]
48
+
49
+ save_current_project_config(project_config)
50
+
51
+
52
+ def clear_history() -> None:
53
+ """Clear all command history for the current project."""
54
+ project_config = get_current_project_config()
55
+ project_config.history = []
56
+ save_current_project_config(project_config)
57
+
58
+
59
+ def remove_from_history(command: str) -> bool:
60
+ """Remove a specific command from history.
61
+
62
+ Args:
63
+ command: The command string to remove from history.
64
+
65
+ Returns:
66
+ True if the command was found and removed, False otherwise.
67
+ """
68
+ if not command or not command.strip():
69
+ return False
70
+
71
+ command = command.strip()
72
+ project_config = get_current_project_config()
73
+ history = project_config.history or []
74
+
75
+ if command in history:
76
+ history.remove(command)
77
+ project_config.history = history
78
+ save_current_project_config(project_config)
79
+ return True
80
+
81
+ return False
82
+
83
+
84
+ def get_history_item(index: int) -> str:
85
+ """Get a specific history item by index.
86
+
87
+ Args:
88
+ index: The index of the history item (0 is most recent).
89
+
90
+ Returns:
91
+ The command string at the specified index, or empty string if not found.
92
+ """
93
+ history = get_history()
94
+ if 0 <= index < len(history):
95
+ return history[index]
96
+ return ""
97
+
98
+
99
+ def search_history(query: str) -> List[str]:
100
+ """Search history for commands containing the query string.
101
+
102
+ Args:
103
+ query: The search query string.
104
+
105
+ Returns:
106
+ List of matching commands, with most recent first.
107
+ """
108
+ if not query or not query.strip():
109
+ return []
110
+
111
+ query = query.strip().lower()
112
+ history = get_history()
113
+
114
+ return [cmd for cmd in history if query in cmd.lower()]
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import sys
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from loguru import logger as _logger
9
+ from minion.const import MINION_ROOT
10
+
11
+ _print_level = "INFO"
12
+
13
+
14
+ def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None):
15
+ """Adjust the log level to above level"""
16
+ global _print_level
17
+ _print_level = print_level
18
+
19
+ current_date = datetime.now()
20
+ formatted_date = current_date.strftime("%Y%m%d")
21
+ log_name = (
22
+ f"{name}_{formatted_date}" if name else formatted_date
23
+ ) # name a log with prefix name
24
+
25
+ _logger.remove()
26
+ _logger.add(sys.stdout, level=print_level)
27
+ _logger.add(MINION_ROOT / f"logs/{log_name}.txt", level=logfile_level)
28
+ return _logger
29
+
30
+
31
+ def setup_tui_logging():
32
+ """Setup logging for TUI mode - removes console output to prevent UI interference"""
33
+ global _print_level
34
+
35
+ # Remove all existing handlers
36
+ _logger.remove()
37
+
38
+ # Only add file logging for TUI mode
39
+ current_date = datetime.now()
40
+ formatted_date = current_date.strftime("%Y%m%d")
41
+ log_name = f"{formatted_date}"
42
+
43
+ # Ensure logs directory exists
44
+ logs_dir = MINION_ROOT / "logs"
45
+ logs_dir.mkdir(exist_ok=True)
46
+
47
+ _logger.add(logs_dir / f"{log_name}.txt", level="DEBUG")
48
+ _print_level = "DEBUG" # Set to DEBUG for file logging
49
+
50
+ return _logger
51
+
52
+
53
+ logger = define_log_level()
@@ -11,12 +11,14 @@ import json
11
11
  import logging
12
12
  import subprocess
13
13
  import asyncio
14
+ import os
14
15
  from pathlib import Path
15
16
  from typing import Dict, List, Any, Optional
16
17
  from dataclasses import dataclass
17
18
 
18
19
  try:
19
20
  from minion.tools.mcp.mcp_toolset import MCPToolset, StdioServerParameters
21
+
20
22
  MCP_AVAILABLE = True
21
23
  except ImportError:
22
24
  MCP_AVAILABLE = False
@@ -26,16 +28,95 @@ except ImportError:
26
28
  logger = logging.getLogger(__name__)
27
29
 
28
30
 
31
+ def find_mcp_config(project_dir: Optional[Path] = None) -> Optional[Path]:
32
+ """
33
+ Auto-discover MCP configuration file.
34
+
35
+ Searches in the following order (first found wins):
36
+
37
+ Project scope (current working directory or specified project_dir):
38
+ 1. .mcp.json
39
+ 2. .claude/mcp.json
40
+ 3. .minion/mcp.json
41
+
42
+ User scope (home directory):
43
+ 4. ~/.claude-code/mcp.json
44
+ 5. ~/.minion-code/mcp.json
45
+ 6. ~/.config/minion-code/mcp.json (XDG standard)
46
+
47
+ Args:
48
+ project_dir: Project directory to search in (defaults to cwd)
49
+
50
+ Returns:
51
+ Path to config file if found, None otherwise
52
+ """
53
+ # Project scope locations
54
+ project_root = project_dir or Path.cwd()
55
+ project_locations = [
56
+ project_root / ".mcp.json",
57
+ project_root / ".claude" / "mcp.json",
58
+ project_root / ".minion" / "mcp.json",
59
+ ]
60
+
61
+ # User scope locations
62
+ home = Path.home()
63
+ user_locations = [
64
+ home / ".claude-code" / "mcp.json",
65
+ home / ".minion-code" / "mcp.json",
66
+ home / ".config" / "minion-code" / "mcp.json",
67
+ ]
68
+
69
+ # Search project scope first
70
+ for config_path in project_locations:
71
+ if config_path.exists():
72
+ logger.info(f"Found MCP config at project scope: {config_path}")
73
+ return config_path
74
+
75
+ # Then search user scope
76
+ for config_path in user_locations:
77
+ if config_path.exists():
78
+ logger.info(f"Found MCP config at user scope: {config_path}")
79
+ return config_path
80
+
81
+ logger.debug("No MCP config file found in any standard location")
82
+ return None
83
+
84
+
85
+ def get_mcp_config_locations() -> Dict[str, List[Path]]:
86
+ """
87
+ Get all standard MCP config file locations.
88
+
89
+ Returns:
90
+ Dictionary with 'project' and 'user' scope locations
91
+ """
92
+ project_root = Path.cwd()
93
+ home = Path.home()
94
+
95
+ return {
96
+ "project": [
97
+ project_root / ".mcp.json",
98
+ project_root / ".claude" / "mcp.json",
99
+ project_root / ".minion" / "mcp.json",
100
+ ],
101
+ "user": [
102
+ home / ".claude-code" / "mcp.json",
103
+ home / ".minion-code" / "mcp.json",
104
+ home / ".config" / "minion-code" / "mcp.json",
105
+ ],
106
+ }
107
+
108
+
29
109
  @dataclass
30
110
  class MCPServerConfig:
31
111
  """Configuration for an MCP server."""
112
+
32
113
  name: str
33
114
  command: str
34
115
  args: List[str]
35
116
  env: Optional[Dict[str, str]] = None
36
117
  disabled: bool = False
37
118
  auto_approve: List[str] = None
38
-
119
+
39
120
  def __post_init__(self):
40
121
  if self.auto_approve is None:
41
122
  self.auto_approve = []
@@ -43,154 +124,171 @@ class MCPServerConfig:
43
124
 
44
125
  class MCPToolsLoader:
45
126
  """Loader for MCP tools from configuration files."""
46
-
47
- def __init__(self, config_path: Optional[Path] = None):
127
+
128
+ def __init__(self, config_path: Optional[Path] = None, auto_discover: bool = True):
48
129
  """
49
130
  Initialize MCP tools loader.
50
-
131
+
51
132
  Args:
52
- config_path: Path to MCP configuration file
133
+ config_path: Path to MCP configuration file. If None and auto_discover=True,
134
+ will search standard locations.
135
+ auto_discover: If True and config_path is None, automatically search for
136
+ config in standard locations (.mcp.json, .claude/, .minion/, etc.)
53
137
  """
54
- self.config_path = config_path
138
+ if config_path:
139
+ self.config_path = config_path
140
+ elif auto_discover:
141
+ self.config_path = find_mcp_config()
142
+ else:
143
+ self.config_path = None
144
+
55
145
  self.servers: Dict[str, MCPServerConfig] = {}
56
146
  self.loaded_tools = []
57
147
  self.toolsets: List[Any] = [] # Store MCPToolset instances for cleanup
58
-
59
- def load_config(self, config_path: Optional[Path] = None) -> Dict[str, MCPServerConfig]:
148
+
149
+ def load_config(
150
+ self, config_path: Optional[Path] = None
151
+ ) -> Dict[str, MCPServerConfig]:
60
152
  """
61
153
  Load MCP configuration from JSON file.
62
-
154
+
63
155
  Args:
64
156
  config_path: Path to configuration file
65
-
157
+
66
158
  Returns:
67
159
  Dictionary of server configurations
68
160
  """
69
161
  if config_path:
70
162
  self.config_path = config_path
71
-
163
+
72
164
  if not self.config_path or not self.config_path.exists():
73
165
  logger.warning(f"MCP config file not found: {self.config_path}")
74
166
  return {}
75
-
167
+
76
168
  try:
77
- with open(self.config_path, 'r', encoding='utf-8') as f:
169
+ with open(self.config_path, "r", encoding="utf-8") as f:
78
170
  config_data = json.load(f)
79
-
80
- servers_config = config_data.get('mcpServers', {})
81
-
171
+
172
+ servers_config = config_data.get("mcpServers", {})
173
+
82
174
  for server_name, server_data in servers_config.items():
83
175
  self.servers[server_name] = MCPServerConfig(
84
176
  name=server_name,
85
- command=server_data.get('command', ''),
86
- args=server_data.get('args', []),
87
- env=server_data.get('env', {}),
88
- disabled=server_data.get('disabled', False),
89
- auto_approve=server_data.get('autoApprove', [])
177
+ command=server_data.get("command", ""),
178
+ args=server_data.get("args", []),
179
+ env=server_data.get("env", {}),
180
+ disabled=server_data.get("disabled", False),
181
+ auto_approve=server_data.get("autoApprove", []),
90
182
  )
91
-
183
+
92
184
  logger.info(f"Loaded {len(self.servers)} MCP server configurations")
93
185
  return self.servers
94
-
186
+
95
187
  except Exception as e:
96
188
  logger.error(f"Failed to load MCP config from {self.config_path}: {e}")
97
189
  return {}
98
-
190
+
99
191
  async def load_tools_from_server(self, server_config: MCPServerConfig) -> List[Any]:
100
192
  """
101
193
  Load tools from an MCP server.
102
-
194
+
103
195
  Args:
104
196
  server_config: Server configuration
105
-
197
+
106
198
  Returns:
107
199
  List of loaded tools
108
200
  """
109
201
  if server_config.disabled:
110
202
  logger.info(f"Skipping disabled MCP server: {server_config.name}")
111
203
  return []
112
-
204
+
113
205
  if not MCP_AVAILABLE:
114
206
  logger.warning("MCP framework not available, skipping MCP server loading")
115
207
  return []
116
-
208
+
117
209
  try:
118
210
  logger.info(f"Loading tools from MCP server: {server_config.name}")
119
- logger.info(f"Command: {server_config.command} {' '.join(server_config.args)}")
120
-
211
+ logger.info(
212
+ f"Command: {server_config.command} {' '.join(server_config.args)}"
213
+ )
214
+
121
215
  # Create MCPToolset with StdioServerParameters
122
216
  toolset = await MCPToolset.create(
123
217
  connection_params=StdioServerParameters(
124
218
  command=server_config.command,
125
219
  args=server_config.args,
126
- env=server_config.env or {}
220
+ env=server_config.env or {},
127
221
  ),
128
222
  name=server_config.name,
129
- structured_output=False # Set to False as requested
223
+ structured_output=False, # Set to False as requested
130
224
  )
131
-
225
+
132
226
  # Store toolset for cleanup
133
227
  self.toolsets.append(toolset)
134
-
228
+
135
229
  # Get tools from the toolset
136
- tools = toolset.tools if hasattr(toolset, 'tools') else []
137
-
138
- logger.info(f"Successfully loaded {len(tools)} tools from {server_config.name}")
230
+ tools = toolset.tools if hasattr(toolset, "tools") else []
231
+
232
+ logger.info(
233
+ f"Successfully loaded {len(tools)} tools from {server_config.name}"
234
+ )
139
235
  return tools
140
-
236
+
141
237
  except Exception as e:
142
- logger.error(f"Failed to load tools from MCP server {server_config.name}: {e}")
238
+ logger.error(
239
+ f"Failed to load tools from MCP server {server_config.name}: {e}"
240
+ )
143
241
  return []
144
-
242
+
145
243
  async def load_all_tools(self) -> List[Any]:
146
244
  """
147
245
  Load tools from all configured MCP servers.
148
-
246
+
149
247
  Returns:
150
248
  List of all loaded MCP tools
151
249
  """
152
250
  all_tools = []
153
-
251
+
154
252
  for server_name, server_config in self.servers.items():
155
253
  if not server_config.disabled:
156
254
  tools = await self.load_tools_from_server(server_config)
157
255
  all_tools.extend(tools)
158
256
  logger.info(f"Loaded {len(tools)} tools from {server_name}")
159
-
257
+
160
258
  self.loaded_tools = all_tools
161
259
  logger.info(f"Total MCP tools loaded: {len(all_tools)}")
162
260
  return all_tools
163
-
261
+
164
262
  def get_server_info(self) -> Dict[str, Dict[str, Any]]:
165
263
  """
166
264
  Get information about configured servers.
167
-
265
+
168
266
  Returns:
169
267
  Dictionary with server information
170
268
  """
171
269
  info = {}
172
270
  for name, config in self.servers.items():
173
271
  info[name] = {
174
- 'command': config.command,
175
- 'args': config.args,
176
- 'disabled': config.disabled,
177
- 'auto_approve_count': len(config.auto_approve)
272
+ "command": config.command,
273
+ "args": config.args,
274
+ "disabled": config.disabled,
275
+ "auto_approve_count": len(config.auto_approve),
178
276
  }
179
277
  return info
180
-
278
+
181
279
  async def close(self):
182
280
  """
183
281
  Close all MCP toolsets and clean up resources.
184
282
  """
185
283
  logger.info(f"Closing {len(self.toolsets)} MCP toolsets...")
186
-
284
+
187
285
  for toolset in self.toolsets:
188
286
  try:
189
287
  await toolset.close()
190
288
  logger.debug(f"Closed toolset: {getattr(toolset, 'name', 'unknown')}")
191
289
  except Exception as e:
192
290
  logger.error(f"Error closing toolset: {e}")
193
-
291
+
194
292
  self.toolsets.clear()
195
293
  logger.info("All MCP toolsets closed")
196
294
 
@@ -199,13 +297,13 @@ class MCPToolsLoader:
199
297
  async def load_mcp_tools(config_path: Path) -> List[Any]:
200
298
  """
201
299
  Convenience function to load MCP tools from a configuration file.
202
-
300
+
203
301
  Args:
204
302
  config_path: Path to MCP configuration file
205
-
303
+
206
304
  Returns:
207
305
  List of loaded MCP tools
208
306
  """
209
307
  loader = MCPToolsLoader(config_path)
210
308
  loader.load_config()
211
- return await loader.load_all_tools()
309
+ return await loader.load_all_tools()