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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skills system for MinionCode
|
|
5
|
+
|
|
6
|
+
Skills are folders of instructions, scripts, and resources that are loaded
|
|
7
|
+
dynamically to improve performance on specialized tasks. Each skill has a
|
|
8
|
+
SKILL.md file with YAML frontmatter containing name and description.
|
|
9
|
+
|
|
10
|
+
Skill search paths (in order):
|
|
11
|
+
- .claude/skills (project-level)
|
|
12
|
+
- ~/.claude/skills (user-level)
|
|
13
|
+
- .minion/skills (project-level)
|
|
14
|
+
- ~/.minion/skills (user-level)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .skill import Skill
|
|
18
|
+
from .skill_registry import SkillRegistry
|
|
19
|
+
from .skill_loader import SkillLoader
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Skill",
|
|
23
|
+
"SkillRegistry",
|
|
24
|
+
"SkillLoader",
|
|
25
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill data class representing a loaded skill.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, Any, List
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Skill:
|
|
16
|
+
"""Represents a loaded skill with its metadata and content."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
content: str # The markdown body (instructions)
|
|
21
|
+
path: Path # Path to the skill directory
|
|
22
|
+
|
|
23
|
+
# Optional metadata from frontmatter
|
|
24
|
+
license: Optional[str] = None
|
|
25
|
+
allowed_tools: List[str] = field(default_factory=list)
|
|
26
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
# Source location type
|
|
29
|
+
location: str = "project" # project, user, managed
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_skill_md(
|
|
33
|
+
cls, skill_md_path: Path, location: str = "project"
|
|
34
|
+
) -> Optional["Skill"]:
|
|
35
|
+
"""
|
|
36
|
+
Parse a SKILL.md file and create a Skill instance.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
skill_md_path: Path to the SKILL.md file
|
|
40
|
+
location: Where the skill was found (project, user, managed)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Skill instance or None if parsing fails
|
|
44
|
+
"""
|
|
45
|
+
if not skill_md_path.exists():
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
content = skill_md_path.read_text(encoding="utf-8")
|
|
49
|
+
frontmatter, body = cls._parse_frontmatter(content)
|
|
50
|
+
|
|
51
|
+
if not frontmatter:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
name = frontmatter.get("name")
|
|
55
|
+
description = frontmatter.get("description")
|
|
56
|
+
|
|
57
|
+
if not name or not description:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
return cls(
|
|
61
|
+
name=name,
|
|
62
|
+
description=description,
|
|
63
|
+
content=body.strip(),
|
|
64
|
+
path=skill_md_path.parent,
|
|
65
|
+
license=frontmatter.get("license"),
|
|
66
|
+
allowed_tools=frontmatter.get("allowed-tools", []) or [],
|
|
67
|
+
metadata=frontmatter.get("metadata", {}) or {},
|
|
68
|
+
location=location,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _parse_frontmatter(content: str) -> tuple[Dict[str, Any], str]:
|
|
73
|
+
"""
|
|
74
|
+
Parse YAML frontmatter from markdown content.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
content: Raw markdown content with potential YAML frontmatter
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple of (frontmatter dict, body content)
|
|
81
|
+
"""
|
|
82
|
+
# Match YAML frontmatter pattern: starts with ---, ends with ---
|
|
83
|
+
pattern = r"^---\s*\n(.*?)\n---\s*\n(.*)$"
|
|
84
|
+
match = re.match(pattern, content, re.DOTALL)
|
|
85
|
+
|
|
86
|
+
if not match:
|
|
87
|
+
return {}, content
|
|
88
|
+
|
|
89
|
+
yaml_content = match.group(1)
|
|
90
|
+
body = match.group(2)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
frontmatter = yaml.safe_load(yaml_content) or {}
|
|
94
|
+
except yaml.YAMLError:
|
|
95
|
+
return {}, content
|
|
96
|
+
|
|
97
|
+
return frontmatter, body
|
|
98
|
+
|
|
99
|
+
def get_prompt(self) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Get the full prompt content for this skill.
|
|
102
|
+
Includes the skill location header for resolving relative paths
|
|
103
|
+
to bundled resources (references/, scripts/, assets/).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Full prompt string with base directory header
|
|
107
|
+
"""
|
|
108
|
+
header = f"""Loading: {self.name}
|
|
109
|
+
Base directory: {self.path}
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
return header + self.content
|
|
113
|
+
|
|
114
|
+
def to_xml(self) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Format skill as XML for inclusion in prompts.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
XML formatted skill entry
|
|
120
|
+
"""
|
|
121
|
+
return f"""<skill>
|
|
122
|
+
<name>{self.name}</name>
|
|
123
|
+
<description>{self.description}</description>
|
|
124
|
+
<location>{self.location}</location>
|
|
125
|
+
</skill>"""
|
|
126
|
+
|
|
127
|
+
def __repr__(self) -> str:
|
|
128
|
+
return f"Skill(name={self.name!r}, location={self.location!r})"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill Loader - discovers and loads skills from standard directories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from .skill import Skill
|
|
13
|
+
from .skill_registry import SkillRegistry, get_skill_registry
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SkillLoader:
|
|
19
|
+
"""
|
|
20
|
+
Discovers and loads skills from standard directories.
|
|
21
|
+
|
|
22
|
+
Search paths (in priority order):
|
|
23
|
+
1. .claude/skills (project-level)
|
|
24
|
+
2. .minion/skills (project-level)
|
|
25
|
+
3. ~/.claude/skills (user-level)
|
|
26
|
+
4. ~/.minion/skills (user-level)
|
|
27
|
+
|
|
28
|
+
Project-level skills override user-level skills with the same name.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Default skill directory names
|
|
32
|
+
SKILL_DIRS = [
|
|
33
|
+
".claude/skills",
|
|
34
|
+
".minion/skills",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
SKILL_FILE = "SKILL.md"
|
|
38
|
+
|
|
39
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the skill loader.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
project_root: Root directory of the project. Defaults to current directory.
|
|
45
|
+
"""
|
|
46
|
+
self.project_root = Path(project_root) if project_root else Path.cwd()
|
|
47
|
+
self.home_dir = Path.home()
|
|
48
|
+
|
|
49
|
+
def get_search_paths(self) -> List[tuple[Path, str]]:
|
|
50
|
+
"""
|
|
51
|
+
Get all skill search paths with their location type.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of (path, location_type) tuples
|
|
55
|
+
"""
|
|
56
|
+
paths = []
|
|
57
|
+
|
|
58
|
+
# Project-level paths (higher priority)
|
|
59
|
+
for skill_dir in self.SKILL_DIRS:
|
|
60
|
+
project_path = self.project_root / skill_dir
|
|
61
|
+
paths.append((project_path, "project"))
|
|
62
|
+
|
|
63
|
+
# User-level paths (lower priority)
|
|
64
|
+
for skill_dir in self.SKILL_DIRS:
|
|
65
|
+
user_path = self.home_dir / skill_dir
|
|
66
|
+
paths.append((user_path, "user"))
|
|
67
|
+
|
|
68
|
+
return paths
|
|
69
|
+
|
|
70
|
+
def discover_skills(self, skills_dir: Path) -> List[Path]:
|
|
71
|
+
"""
|
|
72
|
+
Discover all skill directories within a skills directory.
|
|
73
|
+
|
|
74
|
+
A skill directory must contain a SKILL.md file.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
skills_dir: Directory containing skill subdirectories
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of paths to SKILL.md files
|
|
81
|
+
"""
|
|
82
|
+
if not skills_dir.exists() or not skills_dir.is_dir():
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
skill_files = []
|
|
86
|
+
|
|
87
|
+
for item in skills_dir.iterdir():
|
|
88
|
+
if item.is_dir():
|
|
89
|
+
skill_md = item / self.SKILL_FILE
|
|
90
|
+
if skill_md.exists():
|
|
91
|
+
skill_files.append(skill_md)
|
|
92
|
+
else:
|
|
93
|
+
# Check for nested skill directories (e.g., document-skills/pdf)
|
|
94
|
+
for nested_item in item.iterdir():
|
|
95
|
+
if nested_item.is_dir():
|
|
96
|
+
nested_skill_md = nested_item / self.SKILL_FILE
|
|
97
|
+
if nested_skill_md.exists():
|
|
98
|
+
skill_files.append(nested_skill_md)
|
|
99
|
+
|
|
100
|
+
return skill_files
|
|
101
|
+
|
|
102
|
+
def load_skill(self, skill_md_path: Path, location: str) -> Optional[Skill]:
|
|
103
|
+
"""
|
|
104
|
+
Load a single skill from its SKILL.md file.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
skill_md_path: Path to the SKILL.md file
|
|
108
|
+
location: Location type (project, user, managed)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Skill instance or None if loading fails
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
skill = Skill.from_skill_md(skill_md_path, location)
|
|
115
|
+
if skill:
|
|
116
|
+
logger.debug(f"Loaded skill: {skill.name} from {skill_md_path}")
|
|
117
|
+
else:
|
|
118
|
+
logger.warning(f"Failed to parse skill: {skill_md_path}")
|
|
119
|
+
return skill
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error loading skill from {skill_md_path}: {e}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def load_all(self, registry: Optional[SkillRegistry] = None) -> SkillRegistry:
|
|
125
|
+
"""
|
|
126
|
+
Load all skills from all search paths into the registry.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
registry: Optional registry to load into. Creates new if not provided.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
SkillRegistry containing all loaded skills
|
|
133
|
+
"""
|
|
134
|
+
if registry is None:
|
|
135
|
+
registry = get_skill_registry()
|
|
136
|
+
|
|
137
|
+
for search_path, location in self.get_search_paths():
|
|
138
|
+
skill_files = self.discover_skills(search_path)
|
|
139
|
+
|
|
140
|
+
for skill_md_path in skill_files:
|
|
141
|
+
skill = self.load_skill(skill_md_path, location)
|
|
142
|
+
if skill:
|
|
143
|
+
registered = registry.register(skill)
|
|
144
|
+
if registered:
|
|
145
|
+
logger.info(f"Registered skill: {skill.name} ({location})")
|
|
146
|
+
else:
|
|
147
|
+
logger.debug(
|
|
148
|
+
f"Skipped skill {skill.name} - already registered from higher priority location"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return registry
|
|
152
|
+
|
|
153
|
+
def reload(self, registry: Optional[SkillRegistry] = None) -> SkillRegistry:
|
|
154
|
+
"""
|
|
155
|
+
Reload all skills, clearing the existing registry first.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
registry: Optional registry to reload. Uses global if not provided.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
SkillRegistry containing all reloaded skills
|
|
162
|
+
"""
|
|
163
|
+
if registry is None:
|
|
164
|
+
registry = get_skill_registry()
|
|
165
|
+
|
|
166
|
+
registry.clear()
|
|
167
|
+
return self.load_all(registry)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def load_skills(project_root: Optional[Path] = None) -> SkillRegistry:
|
|
171
|
+
"""
|
|
172
|
+
Convenience function to load all skills.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
project_root: Root directory of the project
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
SkillRegistry containing all loaded skills
|
|
179
|
+
"""
|
|
180
|
+
loader = SkillLoader(project_root)
|
|
181
|
+
return loader.load_all()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_available_skills() -> List[Skill]:
|
|
185
|
+
"""
|
|
186
|
+
Get list of all available skills.
|
|
187
|
+
|
|
188
|
+
Loads skills if the registry is empty.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of available skills
|
|
192
|
+
"""
|
|
193
|
+
registry = get_skill_registry()
|
|
194
|
+
|
|
195
|
+
if len(registry) == 0:
|
|
196
|
+
load_skills()
|
|
197
|
+
|
|
198
|
+
return registry.list_all()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skill Registry - manages loaded skills and provides lookup functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Optional, List
|
|
8
|
+
from .skill import Skill
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SkillRegistry:
|
|
12
|
+
"""
|
|
13
|
+
Registry for managing loaded skills.
|
|
14
|
+
|
|
15
|
+
Skills are organized by name and can be looked up for execution.
|
|
16
|
+
The registry also handles skill deduplication (project skills override user skills).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._skills: Dict[str, Skill] = {}
|
|
21
|
+
self._skills_by_location: Dict[str, List[Skill]] = {
|
|
22
|
+
"project": [],
|
|
23
|
+
"user": [],
|
|
24
|
+
"managed": [],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def register(self, skill: Skill) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Register a skill in the registry.
|
|
30
|
+
|
|
31
|
+
Project skills take precedence over user skills.
|
|
32
|
+
If a skill with the same name already exists from a higher priority location,
|
|
33
|
+
the new skill is not registered.
|
|
34
|
+
|
|
35
|
+
Priority order: project > user > managed
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
skill: Skill instance to register
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if the skill was registered, False if it was skipped
|
|
42
|
+
"""
|
|
43
|
+
existing = self._skills.get(skill.name)
|
|
44
|
+
|
|
45
|
+
if existing:
|
|
46
|
+
# Check priority
|
|
47
|
+
priority = {"project": 0, "user": 1, "managed": 2}
|
|
48
|
+
existing_priority = priority.get(existing.location, 99)
|
|
49
|
+
new_priority = priority.get(skill.location, 99)
|
|
50
|
+
|
|
51
|
+
if new_priority >= existing_priority:
|
|
52
|
+
# Skip - existing skill has higher or equal priority
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
self._skills[skill.name] = skill
|
|
56
|
+
self._skills_by_location[skill.location].append(skill)
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def get(self, name: str) -> Optional[Skill]:
|
|
60
|
+
"""
|
|
61
|
+
Get a skill by name.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: Name of the skill
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Skill instance or None if not found
|
|
68
|
+
"""
|
|
69
|
+
return self._skills.get(name)
|
|
70
|
+
|
|
71
|
+
def exists(self, name: str) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Check if a skill exists in the registry.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: Name of the skill
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if the skill exists
|
|
80
|
+
"""
|
|
81
|
+
return name in self._skills
|
|
82
|
+
|
|
83
|
+
def list_all(self) -> List[Skill]:
|
|
84
|
+
"""
|
|
85
|
+
Get all registered skills.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of all skills
|
|
89
|
+
"""
|
|
90
|
+
return list(self._skills.values())
|
|
91
|
+
|
|
92
|
+
def list_by_location(self, location: str) -> List[Skill]:
|
|
93
|
+
"""
|
|
94
|
+
Get skills by location type.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
location: Location type (project, user, managed)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of skills from that location
|
|
101
|
+
"""
|
|
102
|
+
return self._skills_by_location.get(location, [])
|
|
103
|
+
|
|
104
|
+
def clear(self):
|
|
105
|
+
"""Clear all registered skills."""
|
|
106
|
+
self._skills.clear()
|
|
107
|
+
for location in self._skills_by_location:
|
|
108
|
+
self._skills_by_location[location].clear()
|
|
109
|
+
|
|
110
|
+
def generate_skills_prompt(self, char_budget: int = 10000) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Generate a prompt listing all available skills.
|
|
113
|
+
|
|
114
|
+
Limits output to character budget to manage context window.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
char_budget: Maximum characters for skills list
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Formatted skills prompt
|
|
121
|
+
"""
|
|
122
|
+
skills = self.list_all()
|
|
123
|
+
|
|
124
|
+
if not skills:
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
entries = []
|
|
128
|
+
total_chars = 0
|
|
129
|
+
|
|
130
|
+
for skill in skills:
|
|
131
|
+
entry = skill.to_xml()
|
|
132
|
+
if total_chars + len(entry) > char_budget:
|
|
133
|
+
break
|
|
134
|
+
entries.append(entry)
|
|
135
|
+
total_chars += len(entry)
|
|
136
|
+
|
|
137
|
+
if not entries:
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
skills_xml = "\n".join(entries)
|
|
141
|
+
return f"""<available_skills>
|
|
142
|
+
{skills_xml}
|
|
143
|
+
</available_skills>"""
|
|
144
|
+
|
|
145
|
+
def __len__(self) -> int:
|
|
146
|
+
return len(self._skills)
|
|
147
|
+
|
|
148
|
+
def __contains__(self, name: str) -> bool:
|
|
149
|
+
return name in self._skills
|
|
150
|
+
|
|
151
|
+
def __iter__(self):
|
|
152
|
+
return iter(self._skills.values())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Global skill registry instance
|
|
156
|
+
_skill_registry: Optional[SkillRegistry] = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_skill_registry() -> SkillRegistry:
|
|
160
|
+
"""
|
|
161
|
+
Get the global skill registry instance.
|
|
162
|
+
|
|
163
|
+
Creates the registry if it doesn't exist.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
SkillRegistry instance
|
|
167
|
+
"""
|
|
168
|
+
global _skill_registry
|
|
169
|
+
if _skill_registry is None:
|
|
170
|
+
_skill_registry = SkillRegistry()
|
|
171
|
+
return _skill_registry
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def reset_skill_registry():
|
|
175
|
+
"""Reset the global skill registry."""
|
|
176
|
+
global _skill_registry
|
|
177
|
+
_skill_registry = None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Subagents system for MinionCode
|
|
5
|
+
|
|
6
|
+
Subagents are specialized agent configurations that can be invoked via the Task tool.
|
|
7
|
+
Each subagent has specific tools, system prompts, and use cases.
|
|
8
|
+
|
|
9
|
+
Subagent search paths (in priority order):
|
|
10
|
+
- builtin (code-defined)
|
|
11
|
+
- .claude/subagents or .minion/subagents (project-level, highest priority)
|
|
12
|
+
- ~/.claude/subagents or ~/.minion/subagents (user-level)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .subagent import SubagentConfig
|
|
16
|
+
from .subagent_registry import (
|
|
17
|
+
SubagentRegistry,
|
|
18
|
+
get_subagent_registry,
|
|
19
|
+
reset_subagent_registry,
|
|
20
|
+
)
|
|
21
|
+
from .subagent_loader import SubagentLoader, load_subagents, get_available_subagents
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SubagentConfig",
|
|
25
|
+
"SubagentRegistry",
|
|
26
|
+
"get_subagent_registry",
|
|
27
|
+
"reset_subagent_registry",
|
|
28
|
+
"SubagentLoader",
|
|
29
|
+
"load_subagents",
|
|
30
|
+
"get_available_subagents",
|
|
31
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Built-in subagent configurations."""
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
from ..subagent import SubagentConfig
|
|
7
|
+
|
|
8
|
+
from .general_purpose import get_general_purpose_subagent
|
|
9
|
+
from .explore import get_explore_subagent
|
|
10
|
+
from .plan import get_plan_subagent
|
|
11
|
+
from .claude_code_guide import get_claude_code_guide_subagent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_all_builtin_subagents() -> List[SubagentConfig]:
|
|
15
|
+
"""Get all built-in subagent configurations."""
|
|
16
|
+
return [
|
|
17
|
+
get_general_purpose_subagent(),
|
|
18
|
+
get_explore_subagent(),
|
|
19
|
+
get_plan_subagent(),
|
|
20
|
+
get_claude_code_guide_subagent(),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"get_all_builtin_subagents",
|
|
26
|
+
"get_general_purpose_subagent",
|
|
27
|
+
"get_explore_subagent",
|
|
28
|
+
"get_plan_subagent",
|
|
29
|
+
"get_claude_code_guide_subagent",
|
|
30
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Built-in claude-code-guide subagent configuration."""
|
|
4
|
+
|
|
5
|
+
from ..subagent import SubagentConfig
|
|
6
|
+
|
|
7
|
+
GUIDE_SYSTEM_PROMPT = """You are a documentation lookup specialist for Claude Code and related tools.
|
|
8
|
+
|
|
9
|
+
Your role is to help users understand:
|
|
10
|
+
- How to use Claude Code features and commands
|
|
11
|
+
- Best practices for working with AI coding assistants
|
|
12
|
+
- Tool usage patterns and examples
|
|
13
|
+
- Configuration and customization options
|
|
14
|
+
|
|
15
|
+
Use web search and documentation fetching to find accurate, up-to-date information.
|
|
16
|
+
Always cite your sources with URLs when providing information.
|
|
17
|
+
|
|
18
|
+
Focus on practical, actionable guidance with clear examples."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_claude_code_guide_subagent() -> SubagentConfig:
|
|
22
|
+
"""Get the claude-code-guide subagent configuration."""
|
|
23
|
+
return SubagentConfig(
|
|
24
|
+
name="claude-code-guide",
|
|
25
|
+
description="Documentation lookup agent for Claude Code features, commands, and best practices",
|
|
26
|
+
when_to_use="When you need to look up Claude Code documentation, understand tool usage, or find best practices for AI-assisted coding",
|
|
27
|
+
tools=["web_fetch", "web_search", "file_read"],
|
|
28
|
+
system_prompt=GUIDE_SYSTEM_PROMPT,
|
|
29
|
+
model_name="inherit",
|
|
30
|
+
location="builtin",
|
|
31
|
+
readonly=True,
|
|
32
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Built-in Explore subagent configuration."""
|
|
4
|
+
|
|
5
|
+
from ..subagent import SubagentConfig
|
|
6
|
+
|
|
7
|
+
EXPLORE_SYSTEM_PROMPT = """You are a fast codebase exploration specialist. Your role is to quickly navigate and understand codebases.
|
|
8
|
+
|
|
9
|
+
Your capabilities:
|
|
10
|
+
- Search for files using glob patterns
|
|
11
|
+
- Search for content using grep/regex patterns
|
|
12
|
+
- Read files to understand code structure
|
|
13
|
+
- Fetch web documentation when needed
|
|
14
|
+
- Search the web for relevant information
|
|
15
|
+
|
|
16
|
+
Guidelines:
|
|
17
|
+
- Be thorough but efficient - use glob and grep to narrow down before reading
|
|
18
|
+
- Make multiple parallel tool calls when searching different patterns
|
|
19
|
+
- Summarize findings clearly with file paths and line numbers
|
|
20
|
+
- Do NOT modify any files - you are read-only
|
|
21
|
+
|
|
22
|
+
Focus on answering the user's question with concrete file locations and code references."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_explore_subagent() -> SubagentConfig:
|
|
26
|
+
"""Get the Explore subagent configuration."""
|
|
27
|
+
return SubagentConfig(
|
|
28
|
+
name="Explore",
|
|
29
|
+
description="Fast codebase exploration agent specialized for finding files, searching content, and understanding code structure",
|
|
30
|
+
when_to_use="When you need to quickly explore a codebase, find specific files or patterns, understand code structure, or gather information before making changes",
|
|
31
|
+
tools=["glob", "grep", "file_read", "ls", "web_fetch", "web_search"],
|
|
32
|
+
system_prompt=EXPLORE_SYSTEM_PROMPT,
|
|
33
|
+
model_name="inherit",
|
|
34
|
+
location="builtin",
|
|
35
|
+
readonly=True,
|
|
36
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Built-in general-purpose subagent configuration."""
|
|
4
|
+
|
|
5
|
+
from ..subagent import SubagentConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_general_purpose_subagent() -> SubagentConfig:
|
|
9
|
+
"""Get the general-purpose subagent configuration."""
|
|
10
|
+
return SubagentConfig(
|
|
11
|
+
name="general-purpose",
|
|
12
|
+
description="General-purpose agent for complex tasks requiring full tool access",
|
|
13
|
+
when_to_use="For complex, multi-step tasks that need full tool capabilities including file editing, bash, and code execution",
|
|
14
|
+
tools=["*"],
|
|
15
|
+
system_prompt=None, # Uses default system prompt
|
|
16
|
+
model_name="inherit",
|
|
17
|
+
location="builtin",
|
|
18
|
+
readonly=False,
|
|
19
|
+
)
|